Belajar Elektro

Random post

Belajar Elektro

Powered By Blogger

Tuesday, 21 October 2025

BEL SEKOLAH VERSI 3

 

#include <WiFi.h>

#include <AsyncTCP.h>

#include <ESPAsyncWebServer.h>

#include <LiquidCrystal_I2C.h>

#include <DFRobotDFPlayerMini.h>

#include <ArduinoJson.h>

#include <Preferences.h> // --- LIBRARY BARU: Preferences API ---

#include "time.h"

 

// =======================================================

// --- KONFIGURASI UMUM (HARAP DIUBAH) ---

// =======================================================

 

// --- PIN & PERANGKAT ---

#define I2C_SDA_PIN 21

#define I2C_SCL_PIN 22

#define DFPLAYER_BUSY_PIN 4

 

#define BUTTON_UP_PIN 12

#define BUTTON_DOWN_PIN 13

#define BUTTON_OK_PIN 14

#define BUTTON_BACK_PIN 15

 

// --- KONFIGURASI NVS FLASH (PENGGANTI EEPROM) ---

const char* NVS_NAMESPACE = "bell_config"; // Namespace untuk grup setting

const char* NVS_KEY_JADWAL = "jadwal_data"; // Key untuk data jadwal

 

// --- OBJEK INSIALISASI ---

LiquidCrystal_I2C lcd(0x27, 16, 2);

DFRobotDFPlayerMini myDFPlayer;

HardwareSerial mySoftwareSerial(2); // Serial2 untuk DFPlayer (RX=17, TX=16)

AsyncWebServer server(80);

Preferences preferences; // Objek global Preferences

 

// --- KONFIGURASI JARINGAN & OTENTIKASI (GANTI INI!) ---

const char* ssid = "Tes123";

const char* password = "1234Dcba12";

 

// *** KONFIGURASI USERNAME DAN PASSWORD WEB ***

const char* HTTP_USERNAME = "admin";

const char* HTTP_PASSWORD = "123";  

 

// --- KONFIGURASI WAKTU NTP ---

const char* ntpServer = "pool.ntp.org";

const long  gmtOffset_sec = 7 * 3600; // GMT+7 untuk WIB

const int   daylightOffset_sec = 0;

 

// --- STRUKTUR DATA JADWAL ---

struct JadwalBell {

  int dayOfWeek; // 0=Minggu, 1=Senin, ..., 6=Sabtu

  int jam;

  int menit;

  int track;

};

 

#define MAX_SCHEDULES 50

JadwalBell jadwal[MAX_SCHEDULES];

int currentScheduleCount = 0;

 

// --- VARIABEL GLOBAL & PROTOTIPE (Dihapus) ---

unsigned long lastDisplayUpdate = 0;

unsigned long lastBellCheck = 0;

bool bellTelahBerbunyiHariIni[MAX_SCHEDULES] = {false};

int currentDayOfWeek = 0;

 

// Deklarasi fungsi-fungsi

void handleRoot(AsyncWebServerRequest *request);

void handleJadwalAPI(AsyncWebServerRequest *request);

void handleTimeSync(AsyncWebServerRequest *request);

void notFound(AsyncWebServerRequest *request);

void setupRoutes();

void initWiFi();

void initTime();

void initializeDefaultJadwal();

void loadJadwal();

void saveJadwal();

void checkBell();

void updateDisplay();

void handleButtons();

bool checkAuth(AsyncWebServerRequest *request);

 

// =======================================================

// SETUP DAN LOOP UTAMA

// =======================================================

 

void setup() {

  Serial.begin(115200);

  Serial.println("--- DEBUG SYSTEM START (USING PREFERENCES) ---");

 

  // --- 1. Inisialisasi Perangkat Keras ---

  lcd.begin();

  lcd.backlight();

  lcd.print("Bell Otomatis");

  lcd.setCursor(0, 1);

  lcd.print("Memulai...");

 

  pinMode(DFPLAYER_BUSY_PIN, INPUT);

  pinMode(BUTTON_UP_PIN, INPUT_PULLUP);

  pinMode(BUTTON_DOWN_PIN, INPUT_PULLUP);

  pinMode(BUTTON_OK_PIN, INPUT_PULLUP);

  pinMode(BUTTON_BACK_PIN, INPUT_PULLUP);

 

  // --- 2. Inisialisasi Preferences & Jadwal ---

  if (!preferences.begin(NVS_NAMESPACE, false)) { // Memulai sesi R/W (false)

      Serial.println("FATAL ERROR: Gagal memulai Preferences!");

  } else {

      Serial.printf("DEBUG: Preferences dimulai di namespace: %s\n", NVS_NAMESPACE);

  }

  loadJadwal();

 

  // --- 3. Inisialisasi DFPlayer Mini ---

  mySoftwareSerial.begin(9600, SERIAL_8N1, 17, 16);

  if (!myDFPlayer.begin(mySoftwareSerial)) {

    Serial.println("ERROR: DFPlayer Gagal! Periksa kabel TX/RX.");

  } else {

    Serial.println("DEBUG: DFPlayer OK.");

    myDFPlayer.volume(20);

  }

 

  // --- 4. Inisialisasi WiFi & Waktu ---

  initWiFi();

  initTime();

 

  // --- 5. Inisialisasi Web Server ---

  setupRoutes();

 

  Serial.println("DEBUG: System Ready. Access IP in browser.");

  lcd.clear();

}

 

void loop() {

  if (millis() - lastDisplayUpdate > 1000) {

    updateDisplay();

    lastDisplayUpdate = millis();

  }

  if (millis() - lastBellCheck > 1000) {

    checkBell();

    lastBellCheck = millis();

  }

  handleButtons();

}

 

// =======================================================

// FUNGSI JARINGAN, WAKTU & TAMPILAN (Tidak Berubah)

// =======================================================

 

void initWiFi() {

  // ... (Fungsi initWiFi tetap sama) ...

  lcd.setCursor(0, 1);

  lcd.print("Koneksi...");

  WiFi.begin(ssid, password);

 

  int attempt = 0;

  while (WiFi.status() != WL_CONNECTED && attempt < 20) {

    delay(500);

    Serial.print(".");

    attempt++;

  }

 

  if(WiFi.status() == WL_CONNECTED){

    Serial.print("\nDEBUG: WiFi Terkoneksi. IP: ");

    Serial.println(WiFi.localIP());

    lcd.clear();

    lcd.print("IP:");

    lcd.print(WiFi.localIP());

    delay(1000);

  } else {

    Serial.println("\nERROR: Gagal koneksi WiFi.");

    lcd.setCursor(0, 1);

    lcd.print("WIFI GAGAL!");

    delay(3000);

  }

}

 

void initTime() {

  configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);

  Serial.println("DEBUG: Waktu NTP dikonfigurasi ulang.");

}

 

void updateDisplay() {

  struct tm timeinfo;

  if (!getLocalTime(&timeinfo)) {

    lcd.setCursor(0, 0);

    lcd.print("WAKTU GAGAL SYNC");

    lcd.setCursor(0, 1);

    lcd.print("                ");

    return;

  }

 

  char timeStr[9];

  strftime(timeStr, 9, "%H:%M:%S", &timeinfo);

  lcd.setCursor(0, 0);

  lcd.print(timeStr);

 

  currentDayOfWeek = timeinfo.tm_wday;

  const char* hari[] = {"MIN", "SEN", "SEL", "RAB", "KAM", "JUM", "SAB"};

  lcd.setCursor(10, 0);

  lcd.print(hari[currentDayOfWeek]);

  lcd.print(" ");

 

  char dateStr[11];

  strftime(dateStr, 11, "%d/%m/%y", &timeinfo);

  lcd.setCursor(0, 1);

  lcd.print("Tgl: ");

  lcd.print(dateStr);

  lcd.print("      ");

}

 

void checkBell() {

  struct tm timeinfo;

  if (!getLocalTime(&timeinfo)) return;

 

  int currentHour = timeinfo.tm_hour;

  int currentMinute = timeinfo.tm_min;

  int currentSecond = timeinfo.tm_sec;

  int currentDay = timeinfo.tm_wday;

 

  if (currentHour == 0 && currentMinute == 0 && currentSecond == 0) {

      memset(bellTelahBerbunyiHariIni, false, sizeof(bellTelahBerbunyiHariIni));

  }

 

  for (int i = 0; i < currentScheduleCount; i++) {

   

    if (jadwal[i].dayOfWeek == currentDay &&

        currentHour == jadwal[i].jam &&

        currentMinute == jadwal[i].menit &&

        currentSecond == 0 &&

        !bellTelahBerbunyiHariIni[i])

    {

      Serial.printf("BELL AKTIF: Hari %d, Jam %02d:%02d, Track %d\n", currentDay, currentHour, currentMinute, jadwal[i].track);

     

      lcd.clear();

      lcd.setCursor(0, 0);

      lcd.print("<<< BELL BERBUNYI! >>>");

      lcd.setCursor(0, 1);

      lcd.printf("Track: %d", jadwal[i].track);

     

      myDFPlayer.play(jadwal[i].track);

     

      bellTelahBerbunyiHariIni[i] = true;

      lastDisplayUpdate = millis() + 3000;

    }

  }

}

 

void handleButtons() {

    // Tombol diabaikan dalam mode WebServer

}

 

// =======================================================

// FUNGSI PREFERENCES API (PENGGANTI EEPROM)

// =======================================================

 

void initializeDefaultJadwal() {

    currentScheduleCount = 0;

    if (currentScheduleCount < MAX_SCHEDULES) {

        jadwal[currentScheduleCount++] = {1, 7, 0, 1};

    }

    if (currentScheduleCount < MAX_SCHEDULES) {

        jadwal[currentScheduleCount++] = {5, 15, 30, 2};

    }

    Serial.println("DEBUG: Jadwal default diinisialisasi (2 baris).");

}

 

void loadJadwal() {

    Serial.println("DEBUG: Memulai load Jadwal dari NVS...");

   

    // Mengambil string JSON dari key NVS_KEY_JADWAL. Default adalah string kosong ("")

    String jsonString = preferences.getString(NVS_KEY_JADWAL, "");

   

    int len = jsonString.length();

    Serial.printf("DEBUG: Panjang data yang dibaca dari NVS: %d bytes\n", len);

 

    if (len == 0) {

        Serial.println("WARN: NVS Kosong atau data lama tidak ditemukan. Memuat default.");

        initializeDefaultJadwal();

        return;

    }

   

    Serial.print("DEBUG: JSON String dari NVS: ");

    Serial.println(jsonString);

 

    StaticJsonDocument<4096> doc;

    DeserializationError error = deserializeJson(doc, jsonString);

   

    if (error) {

        Serial.print("ERROR: Gagal deserialize JSON dari NVS (");

        Serial.print(error.c_str());

        Serial.println("). Data rusak. Memuat default.");

        preferences.remove(NVS_KEY_JADWAL); // Hapus data yang rusak

        initializeDefaultJadwal();

        return;

    }

   

    JsonArray array = doc.as<JsonArray>();

    currentScheduleCount = 0;

   

    for (JsonObject obj : array) {

        if (currentScheduleCount < MAX_SCHEDULES) {

            jadwal[currentScheduleCount].dayOfWeek = obj["day"].as<int>();

            jadwal[currentScheduleCount].jam = obj["jam"].as<int>();

            jadwal[currentScheduleCount].menit = obj["menit"].as<int>();

            jadwal[currentScheduleCount].track = obj["track"].as<int>();

            currentScheduleCount++;

        }

    }

    Serial.printf("DEBUG: Jadwal berhasil dimuat dari NVS. Total: %d baris.\n", currentScheduleCount);

}

 

void saveJadwal() {

    Serial.println("DEBUG: Memulai save Jadwal ke NVS...");

   

    StaticJsonDocument<4096> doc; // Kapasitas 4KB

    JsonArray array = doc.to<JsonArray>();

 

    for (int i = 0; i < currentScheduleCount; i++) {

        JsonObject obj = array.add<JsonObject>();

        obj["day"] = jadwal[i].dayOfWeek;

        obj["jam"] = jadwal[i].jam;

        obj["menit"] = jadwal[i].menit;

        obj["track"] = jadwal[i].track;

        Serial.printf("DEBUG: Menyimpan item %d: D:%d, H:%d, M:%d, T:%d\n",

                     i+1, jadwal[i].dayOfWeek, jadwal[i].jam, jadwal[i].menit, jadwal[i].track);

    }

   

    String output;

    serializeJson(doc, output);

   

    Serial.printf("DEBUG: Panjang JSON yang akan disimpan: %d bytes\n", output.length());

 

    // preferences.putString() menulis data ke NVS Flash dengan aman dan cepat

    size_t writtenBytes = preferences.putString(NVS_KEY_JADWAL, output);

 

    if (writtenBytes > 0) {

        Serial.printf("SUCCESS: Jadwal berhasil disimpan dan dikomit ke NVS! %d bytes tertulis.\n", writtenBytes);

    } else {

        Serial.println("ERROR: Gagal menyimpan jadwal ke NVS!");

    }

}

 

// =======================================================

// FUNGSI WEB SERVER ASYNC & HTML (Tidak Berubah)

// =======================================================

 

bool checkAuth(AsyncWebServerRequest *request) {

  if (!request->authenticate(HTTP_USERNAME, HTTP_PASSWORD)) {

    request->requestAuthentication();

    return false;

  }

  return true;

}

 

void setupRoutes() {

  server.on("/", HTTP_GET, handleRoot);

  server.on("/api/jadwal", HTTP_GET, handleJadwalAPI);  

  server.on("/api/jadwal", HTTP_POST, handleJadwalAPI);

  server.on("/api/time_sync", HTTP_POST, handleTimeSync);

  server.onNotFound(notFound);

  server.begin();

}

 

void notFound(AsyncWebServerRequest *request) {

  request->send(404, "text/plain", "Halaman Tidak Ditemukan");

}

 

void handleJadwalAPI(AsyncWebServerRequest *request) {

  if (!checkAuth(request)) return;

 

  if (request->method() == HTTP_GET) {

    StaticJsonDocument<4096> doc;

    JsonArray array = doc.to<JsonArray>();

   

    for (int i = 0; i < currentScheduleCount; i++) {

        JsonObject obj = array.add<JsonObject>();

        obj["day"] = jadwal[i].dayOfWeek;

        obj["jam"] = jadwal[i].jam;

        obj["menit"] = jadwal[i].menit;

        obj["track"] = jadwal[i].track;

    }

   

    String output;

    serializeJson(doc, output);

    request->send(200, "application/json", output);

   

  } else if (request->method() == HTTP_POST) {

    Serial.println("--- DEBUG: Menerima POST request /api/jadwal ---");

    if (request->hasParam("plain", true)) {

      String jsonPayload = request->getParam("plain", true)->value();

     

      Serial.print("DEBUG: Payload diterima: ");

      Serial.println(jsonPayload);

 

      StaticJsonDocument<4096> doc;

      DeserializationError error = deserializeJson(doc, jsonPayload);

 

      if (error) {

        Serial.print("ERROR: Gagal deserialize JSON dari payload: ");

        Serial.println(error.c_str());

        request->send(400, "text/plain", "Gagal memproses JSON: " + String(error.c_str()));

        return;

      }

     

      JsonArray array = doc.as<JsonArray>();

      currentScheduleCount = 0;

     

      for (JsonObject obj : array) {

          if (currentScheduleCount < MAX_SCHEDULES) {

              jadwal[currentScheduleCount].dayOfWeek = obj["day"].as<int>();

              jadwal[currentScheduleCount].jam = obj["jam"].as<int>();

              jadwal[currentScheduleCount].menit = obj["menit"].as<int>();

              jadwal[currentScheduleCount].track = obj["track"].as<int>();

              currentScheduleCount++;

          }

      }

      Serial.printf("DEBUG: JSON berhasil di-parse. Total: %d jadwal.\n", currentScheduleCount);

     

      saveJadwal();

 

      loadJadwal();

      request->send(200, "text/plain", "Jadwal berhasil disimpan permanen!");

     

    } else {

      Serial.println("ERROR: Data POST tidak memiliki parameter 'plain'.");

      request->send(400, "text/plain", "Data POST tidak valid.");

    }

  }

}

 

void handleTimeSync(AsyncWebServerRequest *request) {

  if (!checkAuth(request)) return;

 

  initTime();

  delay(100);

 

  struct tm timeinfo;

  if (getLocalTime(&timeinfo, 5000)) {

    char timeStr[20];

    strftime(timeStr, sizeof(timeStr), "%H:%M:%S %d/%m/%Y", &timeinfo);

   

    String response = "Waktu berhasil disinkronkan: ";

    response += timeStr;

    request->send(200, "text/plain", response);

  } else {

    request->send(500, "text/plain", "Gagal mendapatkan waktu dari NTP setelah sinkronisasi ulang.");

  }

}

 

// =alah satu fungsi web (handleRoot) tetap sama seperti sebelumnya

void handleRoot(AsyncWebServerRequest *request) {

  if (!request->authenticate(HTTP_USERNAME, HTTP_PASSWORD)) {

    request->requestAuthentication();

    return;

  }

 

  String html = R"rawliteral(

<!DOCTYPE html>

<html lang="id">

<head>

    <meta charset="UTF-8">

    <meta name="viewport" content="width=device-width, initial-scale=1.0">

    <title>Kontrol Bell Otomatis ESP32</title>

    <style>

        body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background-color: #f4f7f6; color: #333; margin: 0; padding: 20px; }

        .container { max-width: 900px; margin: auto; background: #fff; padding: 30px; border-radius: 12px; box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1); }

        h1, h2 { color: #007bff; border-bottom: 2px solid #e0e0e0; padding-bottom: 10px; margin-top: 20px; }

        .status-box { background: #e9ecef; padding: 15px; border-radius: 8px; margin-bottom: 20px; display: flex; justify-content: space-between; align-items: center; }

        .status-box p { margin: 0; font-weight: 600; }

        #currentTime { font-size: 1.5em; color: #28a745; }

        .day-section { border: 1px solid #ddd; border-radius: 8px; margin-bottom: 20px; padding: 15px; background: #ffffff; }

        .day-section h3 { background-color: #f0f0f0; padding: 10px; margin: -15px -15px 15px -15px; border-radius: 8px 8px 0 0; display: flex; justify-content: space-between; align-items: center; }

        .day-section table { width: 100%; border-collapse: collapse; }

        .day-section th, .day-section td { padding: 8px; text-align: left; border-bottom: 1px solid #eee; }

        .day-section th { background-color: #007bff; color: white; }

        .btn { padding: 8px 12px; border: none; border-radius: 5px; cursor: pointer; font-weight: bold; transition: background-color 0.3s; margin-left: 5px;}

        .btn-primary { background-color: #007bff; color: white; }

        .btn-success { background-color: #28a745; color: white; }

        .btn-danger { background-color: #dc3545; color: white; }

        .input-time { width: 65px; padding: 5px; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box; }

        #notification { padding: 10px; margin-bottom: 15px; border-radius: 5px; display: none; }

        .success { background-color: #d4edda; color: #155724; border: 1px solid #c3e6cb; }

        .error { background-color: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; }

    </style>

</head>

<body>

    <div class="container">

        <h1>Kontrol Bell Otomatis ESP32</h1>

        <div id="notification"></div>

       

        <h2>Status Perangkat</h2>

        <div class="status-box">

            <p>IP Address: <span id="ipAddress">Sedang Memuat...</span></p>

            <p>Waktu Browser: <span id="currentTime">--:--:--</span></p>

        </div>

 

        <h2>Pengaturan Waktu & Sinkronisasi</h2>

        <div>

            <button class="btn btn-success" onclick="syncTime()">Kalibrasi Waktu (NTP)</button>

            <p style="margin-top:10px; color:#555;">Klik untuk memaksa sinkronisasi ulang waktu ESP32 dengan NTP server.</p>

        </div>

 

        <h2>Jadwal Bell Mingguan</h2>

        <div id="scheduleContainer">

            </div>

 

        <button class="btn btn-success" style="margin-top: 20px; float: right;" onclick="saveSchedule()">SIMPAN JADWAL PERMANEN</button>

       

    </div>

 

    <script>

        const SCHEDULE_API = '/api/jadwal';

        const TIME_API = '/api/time_sync';

        let schedules = [];

        const DAY_NAMES = ["Minggu", "Senin", "Selasa", "Rabu", "Kamis", "Jumat", "Sabtu"];

        const DAY_VALUES = [0, 1, 2, 3, 4, 5, 6];

        let lastId = 0;

 

        function showNotification(message, type) {

            const notif = document.getElementById('notification');

            notif.textContent = message;

            notif.className = '';

            notif.classList.add(type);

            notif.style.display = 'block';

            setTimeout(() => { notif.style.display = 'none'; }, 5000);

        }

 

        function renderSchedule() {

            const container = document.getElementById('scheduleContainer');

            container.innerHTML = '';

 

            DAY_VALUES.forEach(dayIndex => {

                const daySchedules = schedules.filter(sch => sch.dayOfWeek === dayIndex).sort((a, b) => {

                    if (a.jam !== b.jam) return a.jam - b.jam;

                    return a.menit - b.menit;

                });

 

                const section = document.createElement('div');

                section.className = 'day-section';

                section.innerHTML = `

                    <h3>

                        ${DAY_NAMES[dayIndex]}

                        <button class="btn btn-primary" onclick="addNewRow(${dayIndex})">+ Tambah</button>

                    </h3>

                    <table>

                        <thead>

                            <tr>

                                <th>#</th>

                                <th>Jam (0-23)</th>

                                <th>Menit (0-59)</th>

                                <th>Track MP3 (1-255)</th>

                                <th>Aksi</th>

                            </tr>

                        </thead>

                        <tbody id="scheduleBody-${dayIndex}">

                            </tbody>

                    </table>

                `;

                container.appendChild(section);

 

                const body = document.getElementById(`scheduleBody-${dayIndex}`);

                daySchedules.forEach((sch, index) => {

                    const row = body.insertRow();

                    row.innerHTML = `

                        <td>${index + 1}</td>

                        <td><input type="number" min="0" max="23" value="${sch.jam}" class="input-time" onchange="updateScheduleInMemory(this, 'jam', ${sch.id})"></td>

                        <td><input type="number" min="0" max="59" value="${sch.menit}" class="input-time" onchange="updateScheduleInMemory(this, 'menit', ${sch.id})"></td>

                        <td><input type="number" min="1" max="255" value="${sch.track}" class="input-time" onchange="updateScheduleInMemory(this, 'track', ${sch.id})"></td>

                        <td>

                            <button class="btn btn-danger" onclick="deleteRow(${sch.id})">Hapus</button>

                        </td>

                    `;

                });

            });

        }

       

        function processSchedules(rawSchedules) {

            return rawSchedules.map(sch => {

                sch.id = ++lastId;

                return sch;

            });

        }

 

        function updateScheduleInMemory(inputElement, key, schId) {

            let val = inputElement.value;

            const scheduleItem = schedules.find(sch => sch.id === schId);

           

            if (scheduleItem) {

                let intVal = parseInt(val);

 

                if (val === "") {

                    if (key === 'jam') scheduleItem.jam = NaN;

                    else if (key === 'menit') scheduleItem.menit = NaN;

                    else if (key === 'track') scheduleItem.track = NaN;

                    return;

                }

 

                if (isNaN(intVal)) {

                     showNotification("Input harus berupa angka valid.", "error");

                     return;

                }

               

                if (key === 'jam') scheduleItem.jam = intVal;

                else if (key === 'menit') scheduleItem.menit = intVal;

                else if (key === 'track') scheduleItem.track = intVal;

            } else {

                 showNotification("Error: Jadwal tidak ditemukan di memori.", "error");

            }

        }

 

        function addNewRow(dayIndex) {

            if (schedules.length >= 50) {

                 showNotification("Batas maksimum jadwal (50) telah tercapai.", "error");

                 return;

            }

            schedules.push({ dayOfWeek: dayIndex, jam: 7, menit: 0, track: 1, id: ++lastId });

            renderSchedule();

        }

       

        function deleteRow(schId) {

            if (confirm("Yakin ingin menghapus jadwal ini?")) {

                schedules = schedules.filter(sch => sch.id !== schId);

                renderSchedule();

            }

        }

 

        async function fetchSchedule() {

            try {

                const response = await fetch(SCHEDULE_API);

                if (!response.ok) throw new Error("Gagal memuat jadwal dari server.");

               

                const rawSchedules = await response.json();

                schedules = processSchedules(rawSchedules);

               

                renderSchedule();

            } catch (error) {

                showNotification("Gagal memuat jadwal dari NVS: " + error.message, "error");

            }

        }

 

        async function saveSchedule() {

            const finalSchedules = schedules.map(sch => ({

                day: sch.dayOfWeek,

                jam: sch.jam,

                menit: sch.menit,

                track: sch.track

            }));

 

            let validationError = false;

            finalSchedules.forEach((sch, index) => {

                let displayDay = DAY_NAMES[sch.day] ?? "Tidak Diketahui";

                let displayTime = isNaN(sch.jam) || isNaN(sch.menit) ? '??:??' : `${sch.jam}:${sch.menit}`;

               

                if (

                    !(sch.day >= 0 && sch.day <= 6) ||

                    !(sch.jam >= 0 && sch.jam <= 23) ||

                    !(sch.menit >= 0 && sch.menit <= 59) ||

                    !(sch.track >= 1 && sch.track <= 255) ||

                    isNaN(sch.jam) || isNaN(sch.menit) || isNaN(sch.track)

                ) {

                    showNotification(`Jadwal ke-${index + 1} (${displayDay} ${displayTime}) tidak valid. Periksa nilai Jam (0-23), Menit (0-59), atau Track (1-255)!`, "error");

                    validationError = true;

                }

            });

 

            if (validationError) {

                console.log("DEBUG JS: Validasi gagal. Penyimpanan dibatalkan.");

                return;

            }

           

            try {

                const btn = document.querySelector('.btn-success[onclick="saveSchedule()"]');

                btn.disabled = true;

                btn.textContent = "Menyimpan...";

               

                console.log("DEBUG JS: Mengirim data ke server...");

 

                const response = await fetch(SCHEDULE_API, {

                    method: 'POST',

                    headers: { 'Content-Type': 'application/json' },

                    body: JSON.stringify(finalSchedules)

                });

 

                if (!response.ok) {

                    const errorText = await response.text();

                    console.error("DEBUG JS: Server merespon dengan error:", response.status, errorText);

                    throw new Error("Server gagal menyimpan data. Respon: " + errorText);

                }

               

                showNotification("Jadwal Berhasil Disimpan PERMANEN di NVS!", "success");

               

            } catch (error) {

                showNotification("Gagal menyimpan jadwal: " + error.message, "error");

            } finally {

                const btn = document.querySelector('.btn-success[onclick="saveSchedule()"]');

                btn.disabled = false;

                btn.textContent = "SIMPAN JADWAL PERMANEN";

                fetchSchedule();

            }

        }

       

        async function syncTime() {

            if (!confirm("Yakin ingin melakukan kalibrasi waktu dengan NTP?")) return;

            try {

                const btn = document.querySelector('.btn-success[onclick="syncTime()"]');

                btn.disabled = true;

                btn.textContent = "Sinkronisasi...";

               

                const response = await fetch(TIME_API, { method: 'POST' });

 

                if (!response.ok) throw new Error("Gagal sinkronisasi waktu.");

 

                const result = await response.text();

                showNotification("Waktu berhasil disinkronkan. " + result, "success");

               

            } catch (error) {

                showNotification("Gagal sinkronisasi waktu: " + error.message, "error");

            } finally {

                const btn = document.querySelector('.btn-success[onclick="syncTime()"]');

                btn.disabled = false;

                btn.textContent = "Kalibrasi Waktu (NTP)";

            }

        }

       

        function updateCurrentTime() {

            const now = new Date();

            const timeStr = now.toLocaleTimeString('id-ID', { hour: '2-digit', minute: '2-digit', second: '2-digit' });

            document.getElementById('currentTime').textContent = timeStr;

        }

 

        document.addEventListener('DOMContentLoaded', () => {

            document.getElementById('ipAddress').textContent = window.location.host;

            fetchSchedule();

            setInterval(updateCurrentTime, 1000);

        });

    </script>

</body>

</html>

)rawliteral";

  request->send(200, "text/html", html);

}

0 comments: