Belajar Elektro

Random post

Belajar Elektro

Powered By Blogger

This is default featured slide 1 title

Go to Blogger edit html and find these sentences.Now replace these sentences with your own descriptions.This theme is Bloggerized by Lasantha Bandara - Premiumbloggertemplates.com.

This is default featured slide 2 title

Go to Blogger edit html and find these sentences.Now replace these sentences with your own descriptions.This theme is Bloggerized by Lasantha Bandara - Premiumbloggertemplates.com.

This is default featured slide 3 title

Go to Blogger edit html and find these sentences.Now replace these sentences with your own descriptions.This theme is Bloggerized by Lasantha Bandara - Premiumbloggertemplates.com.

This is default featured slide 4 title

Go to Blogger edit html and find these sentences.Now replace these sentences with your own descriptions.This theme is Bloggerized by Lasantha Bandara - Premiumbloggertemplates.com.

This is default featured slide 5 title

Go to Blogger edit html and find these sentences.Now replace these sentences with your own descriptions.This theme is Bloggerized by Lasantha Bandara - Premiumbloggertemplates.com.

Monday, 20 October 2025

BEL OTOMATIS

 

#include <WiFi.h>

#include <ESPAsyncWebServer.h>

#include <LiquidCrystal_I2C.h>

#include <Wire.h>

#include "RTClib.h"        

#include <HardwareSerial.h>

#include <DFRobotDFPlayerMini.h>

#include <SPIFFS.h>

#include <ArduinoJson.h>

#include <NTPClient.h>

#include <WiFiUdp.h>

 

// ===== Konfigurasi =====

const char* ssid = "Tes123";

const char* password = "1234Dcba12";

 

// ===== NTP Client =====

WiFiUDP ntpUDP;

NTPClient timeClient(ntpUDP, "pool.ntp.org", 7 * 3600, 60000); // GMT+7

 

// ===== Web Server =====

AsyncWebServer server(80);

const char* www_username = "admin";

const char* www_password = "1234";

 

// ===== LCD 16x2 =====

LiquidCrystal_I2C lcd(0x27, 16, 2);

 

// ===== RTC =====

RTC_DS3231 rtcDS3231;

RTC_DS1307 rtcDS1307;

bool rtcFound = false;

String rtcType = "Tidak Ada";

bool useDS3231 = false;

bool useDS1307 = false;

 

// ===== DFPlayer =====

HardwareSerial dfSerial(2);

DFRobotDFPlayerMini dfPlayer;

const int ledPin = 2;

const int busyPin = 4; // Pin untuk mendeteksi status busy DFPlayer

int dfVolume = 25;

bool dfPlayerReady = false;

bool isPlaying = false;

 

// ===== Tombol =====

const int btnUp = 12;

const int btnDown = 13;

const int btnOk = 14;

const int btnBack = 15;

 

// Variabel debounce tombol

unsigned long lastDebounceTime = 0;

const unsigned long debounceDelay = 50;

 

// ===== Auto-Sync Configuration =====

bool autoSyncEnabled = true;

unsigned long lastAutoSync = 0;

const unsigned long AUTO_SYNC_INTERVAL = 24 * 3600 * 1000; // 24 jam

const unsigned long AUTO_SYNC_RETRY = 30 * 60 * 1000; // 30 menit jika gagal

 

// ===== Jadwal Bel =====

#define MAX_BEL_PER_DAY 20

struct Bell {

  int hour;

  int minute;

  int track;

  int duration; // detik

  String description;

  bool enabled;

};

 

// JADWAL TERPISAH PER HARI

Bell scheduleSenin[MAX_BEL_PER_DAY];

Bell scheduleSelasa[MAX_BEL_PER_DAY];

Bell scheduleRabu[MAX_BEL_PER_DAY];

Bell scheduleKamis[MAX_BEL_PER_DAY];

Bell scheduleJumat[MAX_BEL_PER_DAY];

Bell scheduleSabtu[MAX_BEL_PER_DAY];

Bell scheduleMinggu[MAX_BEL_PER_DAY];

 

int totalBells[7] = {0};

bool bellPlayed[7][MAX_BEL_PER_DAY] = {false};

 

// Pointer array untuk memudahkan akses

Bell* schedulePointers[7] = {

  scheduleMinggu,    // 0 = Minggu

  scheduleSenin,     // 1 = Senin

  scheduleSelasa,    // 2 = Selasa

  scheduleRabu,      // 3 = Rabu

  scheduleKamis,     // 4 = Kamis

  scheduleJumat,     // 5 = Jumat

  scheduleSabtu      // 6 = Sabtu

};

 

// ===== Variabel Waktu =====

const String daysOfWeek[7] = {"Minggu", "Senin", "Selasa", "Rabu", "Kamis", "Jumat", "Sabtu"};

const String daysOfWeekShort[7] = {"Min", "Sen", "Sel", "Rab", "Kam", "Jum", "Sab"};

const String monthNames[12] = {"Jan", "Feb", "Mar", "Apr", "Mei", "Jun", "Jul", "Agu", "Sep", "Okt", "Nov", "Des"};

 

int currentHour = 0;

int currentMinute = 0;

int currentSecond = 0;

int currentDay = 0;

int currentDate = 0;

int currentMonth = 0;

int currentYear = 0;

 

unsigned long lastNTPUpdate = 0;

const unsigned long NTP_UPDATE_INTERVAL = 3600000; // Update setiap 1 jam

unsigned long lastBellCheck = 0;

const unsigned long BELL_CHECK_INTERVAL = 1000; // Cek bel setiap 1 detik

 

// ===== Status Sistem =====

bool wifiConnected = false;

bool spiffsMounted = false;

unsigned long systemStartTime = 0;

 

// ===== Variabel Uptime =====

unsigned long days = 0;

unsigned long hours = 0;

unsigned long minutes = 0;

unsigned long seconds = 0;

 

// ===== Variabel Menu =====

enum MenuState {

  MAIN_DISPLAY,

  MENU_MAIN,

  MENU_SET_TIME,

  MENU_SET_DATE,

  MENU_SET_BELL,

  MENU_TEST_BELL,

  MENU_SYSTEM_INFO,

  MENU_RTC_SETTINGS,

  MENU_BACKUP_RESTORE,

  MENU_POWER_SETTINGS,

  MENU_ADVANCED_SETTINGS,

  MENU_SELECT_DAY,

  MENU_VIEW_SCHEDULE

};

 

MenuState currentMenuState = MAIN_DISPLAY;

int menuPosition = 0;

int cursorPosition = 0;

bool editing = false;

bool blinkState = false;

unsigned long lastBlinkTime = 0;

const unsigned long BLINK_INTERVAL = 500; // Kedip setiap 500ms

 

// ===== Variabel untuk Menu Jadwal =====

int selectedDay = 0; // Hari yang dipilih untuk dilihat/edit

int schedulePage = 0; // Halaman untuk view schedule

const int SCHEDULE_PER_PAGE = 2; // Jumlah jadwal per halaman di LCD

 

// ===== FITUR BARU: Backup & Restore =====

bool backupInProgress = false;

bool restoreInProgress = false;

String lastBackupTime = "Belum ada";

String lastRestoreTime = "Belum ada";

 

// ===== FITUR BARU: Power Saving =====

bool powerSavingMode = false;

unsigned long lastActivityTime = 0;

const unsigned long POWER_SAVING_TIMEOUT = 5 * 60 * 1000; // 5 menit

bool lcdBacklightOn = true;

unsigned long lastBacklightToggle = 0;

 

// ===== FITUR BARU: System Monitoring =====

unsigned long lastHealthCheck = 0;

const unsigned long HEALTH_CHECK_INTERVAL = 60000; // 1 menit

float systemVoltage = 0.0;

int wifiStrength = 0;

unsigned long memoryUsage = 0;

int cpuLoad = 0;

unsigned long lastMemoryCheck = 0;

 

// ===== FITUR BARU: Notification System =====

struct Notification {

  String message;

  unsigned long timestamp;

  int priority; // 1: Low, 2: Medium, 3: High

};

Notification notifications[10];

int notificationCount = 0;

bool newNotification = false;

 

// ===== FITUR BARU: Emergency Settings =====

bool emergencyMode = false;

String emergencyReason = "";

unsigned long emergencyStartTime = 0;

bool systemLocked = false;

 

// ===== FITUR BARU: Logging System =====

String systemLogs[50];

int logCount = 0;

bool loggingEnabled = true;

 

// ===== Deklarasi Fungsi =====

void saveSchedule();

void loadSchedule();

void setDefaultSchedule();

void addBell(int day, int hour, int minute, int track, int duration, String desc, bool enabled = true);

bool initDFPlayer();

bool initRTC();

void updateTimeFromNTP();

void updateRTCFromNTP();

String formatTime(int h, int m, int s = -1);

String formatDate(int d, int mo, int y, int day);

String formatUptime();

void updateUptime();

void updateLCDDisplay();

Bell getNextBell(int day);

String getHTML();

void setupWebServer();

void handleBellPlaying();

void resetDailyBells();

void printSystemStatus();

void handleButtons();

void displayMenu();

void updateBlink();

void testBellNow();

void detectRTC();

bool initDS3231();

bool initDS1307();

void syncRTCWithNTP();

void readTimeFromRTC();

void checkAutoSync();

void saveRTCTime(int hour, int minute, int date, int month, int year);

 

// ===== FUNGSI BARU =====

void addNotification(String message, int priority = 1);

void clearNotifications();

void displayNotifications();

void checkPowerSaving();

void toggleBacklight();

void performHealthCheck();

void logSystemEvent(String event);

void rotateLogs();

String getSystemLogsHTML();

void backupSystemConfig();

void restoreSystemConfig();

void emergencyShutdown(String reason);

void recoverSystem();

void checkSystemResources();

void displayEmergencyScreen();

void handleAdvancedSettings();

 

// ===== FUNGSI BARU UNTUK JADWAL TERPISAH =====

void displayDaySelection();

void displayScheduleForDay();

void clearDaySchedule(int day);

String getDayScheduleHTML(int day);

void setDefaultScheduleForDay(int day);

void copySchedule(int fromDay, int toDay);

 

// ===== IMPLEMENTASI FUNGSI =====

 

// ===== Format waktu & tanggal =====

String formatTime(int h, int m, int s) {

  String t = (h < 10 ? "0" : "") + String(h) + ":" + (m < 10 ? "0" : "") + String(m);

  if(s >= 0) {

    t += ":";

    t += (s < 10 ? "0" : "") + String(s);

  }

  return t;

}

 

String formatDate(int d, int mo, int y, int day) {

  return daysOfWeek[day].substring(0,3) + ", " + (d < 10 ? "0" : "") + String(d) + " " + monthNames[mo-1] + " " + String(y);

}

 

String formatUptime() {

  updateUptime();

  char buffer[16];

 

  if (days > 0) {

    snprintf(buffer, sizeof(buffer), "%02lud %02luh %02lum", days, hours, minutes);

  } else if (hours > 0) {

    snprintf(buffer, sizeof(buffer), "%02luh %02lum %02lus", hours, minutes, seconds);

  } else {

    snprintf(buffer, sizeof(buffer), "%02lum %02lus", minutes, seconds);

  }

 

  return String(buffer);

}

 

void updateUptime() {

  unsigned long currentMillis = millis();

 

  seconds = currentMillis / 1000;

  minutes = seconds / 60;

  hours = minutes / 60;

  days = hours / 24;

 

  seconds %= 60;

  minutes %= 60;

  hours %= 24;

}

 

// ===== Inisialisasi Tombol =====

void initButtons() {

  pinMode(btnUp, INPUT_PULLUP);

  pinMode(btnDown, INPUT_PULLUP);

  pinMode(btnOk, INPUT_PULLUP);

  pinMode(btnBack, INPUT_PULLUP);

  Serial.println("Tombol diinisialisasi");

}

 

// ===== FITUR BARU: System Logging =====

void logSystemEvent(String event) {

  if (!loggingEnabled) return;

 

  if (logCount >= 50) {

    rotateLogs();

  }

 

  String timestamp = formatTime(currentHour, currentMinute, currentSecond) + " " +

                    formatDate(currentDate, currentMonth, currentYear, currentDay);

 

  systemLogs[logCount] = timestamp + " - " + event;

  logCount++;

 

  Serial.println("[LOG] " + timestamp + " - " + event);

}

 

void rotateLogs() {

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

    systemLogs[i] = systemLogs[i + 1];

  }

  logCount = 49;

}

 

String getSystemLogsHTML() {

  String html = "<div class='logs'><h3>System Logs</h3><div style='max-height: 300px; overflow-y: auto; background: #f8f9fa; padding: 10px; border-radius: 5px; font-family: monospace; font-size: 12px;'>";

 

  for (int i = logCount - 1; i >= 0 && i >= logCount - 20; i--) {

    html += systemLogs[i] + "<br>";

  }

 

  html += "</div></div>";

  return html;

}

 

// ===== FITUR BARU: Notification System =====

void addNotification(String message, int priority) {

  if (notificationCount >= 10) {

    // Geser notifikasi lama

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

      notifications[i] = notifications[i + 1];

    }

    notificationCount = 9;

  }

 

  notifications[notificationCount] = {message, millis(), priority};

  notificationCount++;

  newNotification = true;

 

  logSystemEvent("NOTIF: " + message);

 

  // Tampilkan notifikasi penting di LCD

  if (priority >= 2) {

    lcd.clear();

    lcd.setCursor(0, 0);

    lcd.print("PENTING:");

    lcd.setCursor(0, 1);

    lcd.print(message.substring(0, 16));

    delay(3000);

  }

}

 

void clearNotifications() {

  notificationCount = 0;

  newNotification = false;

}

 

void displayNotifications() {

  // Implementasi display notifications di LCD jika diperlukan

}

 

// ===== FITUR BARU: Power Saving =====

void checkPowerSaving() {

  if (powerSavingMode) {

    unsigned long currentTime = millis();

   

    // Matikan backlight LCD setelah timeout

    if (currentTime - lastActivityTime > POWER_SAVING_TIMEOUT && lcdBacklightOn) {

      lcd.noBacklight();

      lcdBacklightOn = false;

      logSystemEvent("Power saving: LCD backlight OFF");

    }

   

    // Blink LED sparingly dalam power saving

    if (currentTime - lastBacklightToggle > 10000) {

      digitalWrite(ledPin, !digitalRead(ledPin));

      lastBacklightToggle = currentTime;

    }

  }

}

 

void toggleBacklight() {

  if (lcdBacklightOn) {

    lcd.noBacklight();

    lcdBacklightOn = false;

  } else {

    lcd.backlight();

    lcdBacklightOn = true;

  }

  lastActivityTime = millis();

}

 

// ===== FITUR BARU: Health Check =====

void performHealthCheck() {

  // Check WiFi strength

  if (wifiConnected) {

    wifiStrength = WiFi.RSSI();

  }

 

  // Check memory usage (estimasi)

  memoryUsage = ESP.getFreeHeap();

 

  // Check system stability

  unsigned long currentUptime = millis() - systemStartTime;

 

  // Add notifications jika ada masalah

  if (wifiStrength < -80 && wifiConnected) {

    addNotification("Sinyal WiFi lemah", 2);

  }

 

  if (memoryUsage < 10000) {

    addNotification("Memory rendah", 2);

  }

 

  if (!rtcFound && !wifiConnected) {

    addNotification("Tidak ada sumber waktu", 3);

  }

 

  logSystemEvent("Health check: WiFi=" + String(wifiStrength) + "dBm, Memory=" + String(memoryUsage));

}

 

// ===== FITUR BARU: Backup & Restore =====

void backupSystemConfig() {

  if (backupInProgress) return;

 

  backupInProgress = true;

  logSystemEvent("Memulai backup sistem");

 

  // Simpan konfigurasi tambahan

  DynamicJsonDocument doc(4096);

  doc["volume"] = dfVolume;

  doc["autoSyncEnabled"] = autoSyncEnabled;

  doc["powerSavingMode"] = powerSavingMode;

  doc["backupTime"] = formatTime(currentHour, currentMinute) + " " + formatDate(currentDate, currentMonth, currentYear, currentDay);

 

  File file = SPIFFS.open("/system_config.json", "w");

  if (file) {

    serializeJson(doc, file);

    file.close();

    lastBackupTime = formatTime(currentHour, currentMinute) + " " + formatDate(currentDate, currentMonth, currentYear, currentDay);

    addNotification("Backup berhasil", 1);

    logSystemEvent("Backup sistem berhasil");

  } else {

    addNotification("Backup gagal", 2);

    logSystemEvent("Backup sistem gagal");

  }

 

  backupInProgress = false;

}

 

void restoreSystemConfig() {

  if (restoreInProgress) return;

 

  restoreInProgress = true;

  logSystemEvent("Memulai restore sistem");

 

  if (SPIFFS.exists("/system_config.json")) {

    File file = SPIFFS.open("/system_config.json", "r");

    if (file) {

      DynamicJsonDocument doc(4096);

      DeserializationError error = deserializeJson(doc, file);

      if (!error) {

        if (doc.containsKey("volume")) {

          dfVolume = doc["volume"];

          if (dfPlayerReady) dfPlayer.volume(dfVolume);

        }

        if (doc.containsKey("autoSyncEnabled")) {

          autoSyncEnabled = doc["autoSyncEnabled"];

        }

        if (doc.containsKey("powerSavingMode")) {

          powerSavingMode = doc["powerSavingMode"];

        }

       

        lastRestoreTime = formatTime(currentHour, currentMinute) + " " + formatDate(currentDate, currentMonth, currentYear, currentDay);

        addNotification("Restore berhasil", 1);

        logSystemEvent("Restore sistem berhasil");

      }

      file.close();

    }

  } else {

    addNotification("File backup tidak ada", 2);

  }

 

  restoreInProgress = false;

}

 

// ===== FITUR BARU: Emergency System =====

void emergencyShutdown(String reason) {

  emergencyMode = true;

  emergencyReason = reason;

  emergencyStartTime = millis();

  systemLocked = true;

 

  logSystemEvent("EMERGENCY: " + reason);

  addNotification("EMERGENCY: " + reason, 3);

 

  // Stop semua bell yang sedang berjalan

  if (dfPlayerReady) {

    dfPlayer.stop();

  }

  digitalWrite(ledPin, LOW);

  isPlaying = false;

}

 

void recoverSystem() {

  emergencyMode = false;

  emergencyReason = "";

  systemLocked = false;

 

  logSystemEvent("System recovered dari emergency");

  addNotification("System recovered", 1);

}

 

void checkSystemResources() {

  // Check free heap memory

  if (ESP.getFreeHeap() < 10000) {

    emergencyShutdown("Memory kritikal");

    return;

  }

 

  // Check jika system hang (uptime terlalu lama tanpa reset)

  if (millis() - systemStartTime > 7 * 24 * 3600 * 1000) { // 7 hari

    addNotification("Disarankan restart sistem", 2);

  }

}

 

void displayEmergencyScreen() {

  lcd.clear();

  lcd.setCursor(0, 0);

  lcd.print("!!! EMERGENCY !!!");

  lcd.setCursor(0, 1);

  lcd.print(emergencyReason.substring(0, 16));

}

 

void handleAdvancedSettings() {

  // Implementasi advanced settings jika diperlukan

}

 

// ===== FUNGSI BARU: Management Jadwal Per Hari =====

void clearDaySchedule(int day) {

  if (day >= 0 && day < 7) {

    totalBells[day] = 0;

    logSystemEvent("Jadwal hari " + daysOfWeek[day] + " dikosongkan");

  }

}

 

void setDefaultScheduleForDay(int day) {

  if (day < 0 || day > 6) return;

 

  clearDaySchedule(day);

 

  switch(day) {

    case 1: // SENIN - Jadwal Sekolah

      addBell(1, 6, 30, 1, 10, "Bangun Pagi");

      addBell(1, 7, 0, 2, 15, "Persiapan Sekolah");

      addBell(1, 7, 30, 3, 20, "Masuk Sekolah");

      addBell(1, 9, 30, 4, 10, "Istirahat 1");

      addBell(1, 10, 0, 5, 15, "Jam ke-3");

      addBell(1, 11, 30, 6, 10, "Istirahat 2");

      addBell(1, 12, 0, 7, 15, "Jam ke-4");

      addBell(1, 13, 30, 8, 20, "Pulang Sekolah");

      break;

     

    case 2: // SELASA - Jadwal Sekolah

      addBell(2, 6, 30, 1, 10, "Bangun Pagi");

      addBell(2, 7, 0, 2, 15, "Persiapan Sekolah");

      addBell(2, 7, 30, 3, 20, "Masuk Sekolah");

      addBell(2, 10, 15, 9, 10, "Istirahat");

      addBell(2, 12, 15, 10, 15, "Jam ke-5");

      addBell(2, 14, 0, 8, 20, "Pulang Sekolah");

      break;

     

    case 3: // RABU - Jadwal Setengah Hari

      addBell(3, 6, 30, 1, 10, "Bangun Pagi");

      addBell(3, 7, 0, 2, 15, "Persiapan Sekolah");

      addBell(3, 7, 30, 3, 20, "Masuk Sekolah");

      addBell(3, 10, 0, 4, 10, "Istirahat");

      addBell(3, 12, 0, 8, 20, "Pulang Sekolah");

      break;

     

    case 4: // KAMIS - Jadwal Sekolah

      addBell(4, 6, 30, 1, 10, "Bangun Pagi");

      addBell(4, 7, 0, 2, 15, "Persiapan Sekolah");

      addBell(4, 7, 30, 3, 20, "Masuk Sekolah");

      addBell(4, 9, 0, 11, 10, "Upacara");

      addBell(4, 11, 30, 6, 10, "Istirahat");

      addBell(4, 13, 30, 8, 20, "Pulang Sekolah");

      break;

      

    case 5: // JUMAT - Jadwal Khusus

      addBell(5, 6, 0, 12, 10, "Sholat Subuh");

      addBell(5, 6, 30, 1, 10, "Bangun Pagi");

      addBell(5, 7, 30, 3, 20, "Masuk Sekolah");

      addBell(5, 11, 0, 13, 15, "Sholat Jumat");

      addBell(5, 13, 0, 8, 20, "Pulang Sekolah");

      break;

     

    case 6: // SABTU - Kegiatan Ekstra

      addBell(6, 7, 0, 1, 10, "Bangun Pagi");

      addBell(6, 8, 0, 14, 15, "Ekstrakurikuler");

      addBell(6, 10, 0, 4, 10, "Istirahat");

      addBell(6, 12, 0, 8, 20, "Selesai");

      break;

     

    case 0: // MINGGU - Libur

      addBell(0, 6, 0, 12, 10, "Sholat Subuh");

      addBell(0, 12, 0, 15, 10, "Sholat Dzuhur");

      addBell(0, 15, 0, 16, 10, "Sholat Ashar");

      addBell(0, 18, 0, 17, 10, "Sholat Maghrib");

      addBell(0, 19, 0, 18, 10, "Sholat Isya");

      break;

  }

}

 

void copySchedule(int fromDay, int toDay) {

  if (fromDay < 0 || fromDay > 6 || toDay < 0 || toDay > 6) return;

 

  clearDaySchedule(toDay);

 

  for (int i = 0; i < totalBells[fromDay]; i++) {

    Bell bell = schedulePointers[fromDay][i];

    addBell(toDay, bell.hour, bell.minute, bell.track, bell.duration, bell.description, bell.enabled);

  }

 

  logSystemEvent("Jadwal " + daysOfWeek[fromDay] + " disalin ke " + daysOfWeek[toDay]);

}

 

// ===== FUNGSI BARU: Display Day Selection =====

void displayDaySelection() {

  lcd.clear();

  lcd.setCursor(0, 0);

  lcd.print("PILIH HARI");

  lcd.setCursor(0, 1);

  lcd.print("> ");

  lcd.print(daysOfWeek[menuPosition]);

}

 

// ===== FUNGSI BARU: Display Schedule for Selected Day =====

void displayScheduleForDay() {

  lcd.clear();

  lcd.setCursor(0, 0);

  lcd.print(daysOfWeek[selectedDay]);

  lcd.print(" P");

  lcd.print(schedulePage + 1);

 

  Bell* daySchedule = schedulePointers[selectedDay];

  int startIndex = schedulePage * SCHEDULE_PER_PAGE;

 

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

    int index = startIndex + i;

    if (index < totalBells[selectedDay]) {

      lcd.setCursor(0, 1 + i);

      lcd.print(formatTime(daySchedule[index].hour, daySchedule[index].minute));

      lcd.print(" ");

      lcd.print(daySchedule[index].description.substring(0, 8));

    }

  }

}

 

// ===== MODIFIKASI: Fungsi Add Bell untuk struktur baru =====

void addBell(int day, int hour, int minute, int track, int duration, String desc, bool enabled) {

  if(day >= 0 && day < 7 && totalBells[day] < MAX_BEL_PER_DAY) {

    Bell* daySchedule = schedulePointers[day];

    daySchedule[totalBells[day]] = {hour, minute, track, duration, desc, enabled};

    totalBells[day]++;

  }

}

 

// ===== MODIFIKASI: Fungsi Save Schedule untuk struktur baru =====

void saveSchedule() {

  DynamicJsonDocument doc(16384); // Increase size for separate schedules

  JsonObject scheduleObj = doc.createNestedObject("schedule");

 

  for(int d = 0; d < 7; d++) {

    JsonArray dayArr = scheduleObj.createNestedArray(daysOfWeekShort[d]);

    Bell* daySchedule = schedulePointers[d];

   

    for(int i = 0; i < totalBells[d]; i++) {

      JsonObject obj = dayArr.createNestedObject();

      obj["hour"] = daySchedule[i].hour;

      obj["minute"] = daySchedule[i].minute;

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

      obj["duration"] = daySchedule[i].duration;

      obj["description"] = daySchedule[i].description;

      obj["enabled"] = daySchedule[i].enabled;

    }

  }

 

  doc["volume"] = dfVolume;

  doc["autoSyncEnabled"] = autoSyncEnabled;

  doc["powerSavingMode"] = powerSavingMode;

 

  File file = SPIFFS.open("/schedule.json", "w");

  if(file) {

    serializeJson(doc, file);

    file.close();

    Serial.println("Jadwal per hari disimpan ke SPIFFS");

    logSystemEvent("Jadwal per hari disimpan");

  } else {

    Serial.println("Gagal menyimpan jadwal!");

    logSystemEvent("Gagal menyimpan jadwal");

  }

}

 

// ===== MODIFIKASI: Fungsi Load Schedule untuk struktur baru =====

void loadSchedule() {

  if(!SPIFFS.exists("/schedule.json")) {

    Serial.println("File jadwal tidak ada, buat jadwal default per hari");

    logSystemEvent("Buat jadwal default per hari");

    setDefaultSchedule();

    saveSchedule();

    return;

  }

 

  File file = SPIFFS.open("/schedule.json", "r");

  if(file) {

    DynamicJsonDocument doc(16384);

    DeserializationError error = deserializeJson(doc, file);

    if(!error) {

      // Reset semua bell

      for(int d = 0; d < 7; d++) totalBells[d] = 0;

     

      JsonObject scheduleObj = doc["schedule"];

      for(int d = 0; d < 7; d++) {

        String dayKey = daysOfWeekShort[d];

        if(scheduleObj.containsKey(dayKey)) {

          JsonArray arr = scheduleObj[dayKey];

          for(JsonObject obj : arr) {

            int hour = obj["hour"];

            int minute = obj["minute"];

            int track = obj["track"];

            int duration = obj["duration"];

            String description = obj["description"].as<String>();

            bool enabled = obj.containsKey("enabled") ? obj["enabled"] : true;

           

            addBell(d, hour, minute, track, duration, description, enabled);

          }

        }

      }

     

      if(doc.containsKey("volume")) {

        dfVolume = doc["volume"];

        if(dfPlayerReady) dfPlayer.volume(dfVolume);

      }

     

      if(doc.containsKey("autoSyncEnabled")) {

        autoSyncEnabled = doc["autoSyncEnabled"];

      }

     

      if(doc.containsKey("powerSavingMode")) {

        powerSavingMode = doc["powerSavingMode"];

      }

     

      Serial.println("Jadwal per hari berhasil di-restore");

      logSystemEvent("Jadwal per hari di-restore");

    } else {

      Serial.println("Gagal membaca file JSON!");

      logSystemEvent("Gagal baca JSON, buat jadwal default");

      setDefaultSchedule();

    }

    file.close();

  } else {

    Serial.println("Gagal membuka file jadwal!");

    logSystemEvent("Gagal buka file jadwal");

    setDefaultSchedule();

  }

}

 

// ===== MODIFIKASI: Set Default Schedule untuk struktur baru =====

void setDefaultSchedule() {

  for(int d = 0; d < 7; d++) {

    setDefaultScheduleForDay(d);

  }

}

 

// ===== MODIFIKASI: Get Next Bell untuk struktur baru =====

Bell getNextBell(int day) {

  Bell nextBell = {-1, -1, -1, -1, "", true};

 

  if (day < 0 || day > 6) return nextBell;

 

  Bell* daySchedule = schedulePointers[day];

 

  for(int i = 0; i < totalBells[day]; i++) {

    if(daySchedule[i].enabled) {

      if(daySchedule[i].hour > currentHour ||

         (daySchedule[i].hour == currentHour && daySchedule[i].minute > currentMinute)) {

        if(nextBell.hour == -1 ||

           (daySchedule[i].hour < nextBell.hour ||

           (daySchedule[i].hour == nextBell.hour && daySchedule[i].minute < nextBell.minute))) {

          nextBell = daySchedule[i];

        }

      }

    }

  }

  return nextBell;

}

 

// ===== MODIFIKASI: Handle Bell Playing untuk struktur baru =====

void handleBellPlaying() {

  static unsigned long bellStartTime = 0;

  static int currentBellDay = -1;

  static int currentBellIndex = -1;

 

  if(currentBellDay != currentDay) {

    resetDailyBells();

    currentBellDay = currentDay;

  }

 

  if (currentDay < 0 || currentDay > 6) return;

 

  Bell* daySchedule = schedulePointers[currentDay];

 

  for(int i = 0; i < totalBells[currentDay]; i++) {

    if(daySchedule[i].enabled &&

       currentHour == daySchedule[i].hour &&

       currentMinute == daySchedule[i].minute &&

       currentSecond == 0 &&

       !bellPlayed[currentDay][i]) {

     

      if(dfPlayerReady) {

        dfPlayer.play(daySchedule[i].track);

        digitalWrite(ledPin, HIGH);

        isPlaying = true;

        bellStartTime = millis();

      }

     

      lcd.clear();

      lcd.setCursor(0, 0);

      lcd.print("BEL: " + daySchedule[i].description);

      lcd.setCursor(0, 1);

      lcd.print("Track: " + String(daySchedule[i].track));

     

      Serial.println("Bell: " + daySchedule[i].description +

                    " Track: " + String(daySchedule[i].track));

      logSystemEvent("Bell: " + daySchedule[i].description + " Track: " + String(daySchedule[i].track));

     

      bellPlayed[currentDay][i] = true;

      currentBellIndex = i;

    }

  }

 

  if(isPlaying && currentBellIndex != -1) {

    Bell* daySchedule = schedulePointers[currentDay];

    if(millis() - bellStartTime > (daySchedule[currentBellIndex].duration * 1000)) {

      digitalWrite(ledPin, LOW);

      isPlaying = false;

      currentBellIndex = -1;

    }

  }

}

 

// ===== Deteksi RTC =====

void detectRTC() {

  Serial.println("Mendeteksi modul RTC...");

 

  // Coba deteksi DS3231 terlebih dahulu

  if (initDS3231()) {

    rtcFound = true;

    useDS3231 = true;

    rtcType = "DS3231";

    Serial.println(" RTC DS3231 terdeteksi");

    return;

  }

 

  // Jika DS3231 tidak ditemukan, coba DS1307

  if (initDS1307()) {

    rtcFound = true;

    useDS1307 = true;

    rtcType = "DS1307";

    Serial.println(" RTC DS1307 terdeteksi");

    return;

  }

 

  // Jika kedua RTC tidak ditemukan

  rtcFound = false;

  useDS3231 = false;

  useDS1307 = false;

  rtcType = "Tidak Ada";

  Serial.println(" Tidak ada modul RTC yang terdeteksi");

}

 

// ===== Inisialisasi DS3231 =====

bool initDS3231() {

  Wire.begin();

  delay(100);

 

  if (!rtcDS3231.begin()) {

    return false;

  }

 

  // Cek jika RTC kehilangan power

  if (rtcDS3231.lostPower()) {

    Serial.println(" DS3231 kehilangan power, set waktu default");

    rtcDS3231.adjust(DateTime(2023, 1, 1, 0, 0, 0));

  }

 

  // Test baca waktu

  DateTime now = rtcDS3231.now();

  Serial.print("Waktu DS3231: ");

  Serial.print(now.year(), DEC);

  Serial.print('/');

  Serial.print(now.month(), DEC);

  Serial.print('/');

  Serial.print(now.day(), DEC);

  Serial.print(' ');

  Serial.print(now.hour(), DEC);

  Serial.print(':');

  Serial.print(now.minute(), DEC);

  Serial.print(':');

  Serial.print(now.second(), DEC);

  Serial.println();

 

  return true;

}

 

// ===== Inisialisasi DS1307 =====

bool initDS1307() {

  Wire.begin();

  delay(100);

 

  if (!rtcDS1307.begin()) {

    return false;

  }

 

  // Cek jika RTC tidak berjalan

  if (!rtcDS1307.isrunning()) {

    Serial.println(" DS1307 tidak berjalan, set waktu default");

    rtcDS1307.adjust(DateTime(2023, 1, 1, 0, 0, 0));

  }

 

  // Test baca waktu

  DateTime now = rtcDS1307.now();

  Serial.print("Waktu DS1307: ");

  Serial.print(now.year(), DEC);

  Serial.print('/');

  Serial.print(now.month(), DEC);

  Serial.print('/');

  Serial.print(now.day(), DEC);

  Serial.print(' ');

  Serial.print(now.hour(), DEC);

  Serial.print(':');

  Serial.print(now.minute(), DEC);

  Serial.print(':');

  Serial.print(now.second(), DEC);

  Serial.println();

 

  return true;

}

 

// ===== Inisialisasi RTC =====

bool initRTC() {

  detectRTC();

  return rtcFound;

}

 

// ===== Baca Waktu dari RTC =====

void readTimeFromRTC() {

  if (useDS3231) {

    DateTime now = rtcDS3231.now();

    currentHour = now.hour();

    currentMinute = now.minute();

    currentSecond = now.second();

    currentDay = now.dayOfTheWeek();

    currentDate = now.day();

    currentMonth = now.month();

    currentYear = now.year();

  } else if (useDS1307) {

    DateTime now = rtcDS1307.now();

    currentHour = now.hour();

    currentMinute = now.minute();

    currentSecond = now.second();

    currentDay = now.dayOfTheWeek();

    currentDate = now.day();

    currentMonth = now.month();

    currentYear = now.year();

  }

}

 

// ===== Sync RTC dengan NTP =====

void syncRTCWithNTP() {

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

    if (timeClient.forceUpdate()) {

      unsigned long epochTime = timeClient.getEpochTime();

     

      if (useDS3231) {

        rtcDS3231.adjust(DateTime(epochTime));

        Serial.println(" DS3231 disinkronisasi dengan NTP");

      } else if (useDS1307) {

        rtcDS1307.adjust(DateTime(epochTime));

        Serial.println(" DS1307 disinkronisasi dengan NTP");

      }

     

      // Update variabel waktu

      readTimeFromRTC();

    }

  }

}

 

// ===== Simpan Waktu ke RTC =====

void saveRTCTime(int hour, int minute, int date, int month, int year) {

  if (rtcFound) {

    if (useDS3231) {

      rtcDS3231.adjust(DateTime(year, month, date, hour, minute, 0));

    } else if (useDS1307) {

      rtcDS1307.adjust(DateTime(year, month, date, hour, minute, 0));

    }

    Serial.println("💾 Waktu disimpan ke RTC: " +

                  String(hour) + ":" + String(minute) + " " +

                  String(date) + "/" + String(month) + "/" + String(year));

  }

}

 

// ===== Inisialisasi DFPlayer =====

bool initDFPlayer() {

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

  delay(1000);

 

  pinMode(busyPin, INPUT);

 

  for(int attempts = 0; attempts < 5; attempts++) {

    if(dfPlayer.begin(dfSerial)) {

      dfPlayer.volume(dfVolume);

      dfPlayer.EQ(0);

      dfPlayer.outputDevice(DFPLAYER_DEVICE_SD);

      Serial.println("DFPlayer siap");

      return true;

    }

    Serial.println("DFPlayer gagal, coba lagi...");

    delay(2000);

  }

  return false;

}

 

// ===== Update Waktu dari NTP =====

void updateTimeFromNTP() {

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

    Serial.println("Mengupdate waktu dari NTP...");

    if(timeClient.update()) {

      currentHour = timeClient.getHours();

      currentMinute = timeClient.getMinutes();

      currentSecond = timeClient.getSeconds();

      currentDay = timeClient.getDay();

     

      unsigned long epochTime = timeClient.getEpochTime();

      struct tm *ptm = gmtime((time_t *)&epochTime);

      currentDate = ptm->tm_mday;

      currentMonth = ptm->tm_mon + 1;

      currentYear = ptm->tm_year + 1900;

     

      lastNTPUpdate = millis();

      Serial.println("Waktu NTP diperbarui: " + formatTime(currentHour, currentMinute, currentSecond));

    }

  }

}

 

void updateRTCFromNTP() {

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

    syncRTCWithNTP();

  }

}

 

// ===== Auto-Sync RTC dengan NTP =====

void checkAutoSync() {

  if (autoSyncEnabled && wifiConnected && rtcFound) {

    unsigned long currentTime = millis();

   

    // Cek jika sudah waktunya sync (24 jam) atau retry (30 menit)

    if ((currentTime - lastAutoSync > AUTO_SYNC_INTERVAL) ||

        (lastAutoSync == 0 && currentTime > AUTO_SYNC_RETRY)) {

     

      Serial.println("🔄 Memeriksa auto-sync RTC dengan NTP...");

      logSystemEvent("Memeriksa auto-sync RTC dengan NTP");

     

      if (timeClient.forceUpdate()) {

        syncRTCWithNTP();

        lastAutoSync = currentTime;

        Serial.println(" Auto-sync RTC dengan NTP berhasil");

        logSystemEvent("Auto-sync RTC dengan NTP berhasil");

       

        // Tampilkan notifikasi di LCD

        lcd.clear();

        lcd.setCursor(0, 0);

        lcd.print("AUTO-SYNC OK");

        lcd.setCursor(0, 1);

        lcd.print(formatTime(currentHour, currentMinute));

        delay(2000);

      } else {

        Serial.println(" Gagal auto-sync, coba lagi dalam 30 menit");

        logSystemEvent("Gagal auto-sync, coba lagi dalam 30 menit");

        lastAutoSync = currentTime - AUTO_SYNC_INTERVAL + AUTO_SYNC_RETRY;

      }

    }

  }

}

 

// ===== Reset Daily Bells =====

void resetDailyBells() {

  for(int d = 0; d < 7; d++) {

    for(int i = 0; i < totalBells[d]; i++) {

      bellPlayed[d][i] = false;

    }

  }

  Serial.println("Status bell direset untuk hari baru");

}

 

// ===== Test Bell =====

void testBellNow() {

  if (dfPlayerReady) {

    dfPlayer.play(1); // Play track 1

    Serial.println("Test bell dimainkan");

    lcd.clear();

    lcd.setCursor(0, 0);

    lcd.print("TEST BELL");

    lcd.setCursor(0, 1);

    lcd.print("Track 1");

    delay(2000);

    currentMenuState = MAIN_DISPLAY; // Kembali ke tampilan utama

  }

}

 

// ===== Update Blink =====

void updateBlink() {

  if (millis() - lastBlinkTime > BLINK_INTERVAL) {

    blinkState = !blinkState;

    lastBlinkTime = millis();

  }

}

 

// ===== MODIFIKASI: LCD Display - Hanya Jam & Tanggal di Main Display =====

void updateLCDDisplay() {

  static unsigned long lastDisplayChange = 0;

 

  if (emergencyMode) {

    displayEmergencyScreen();

    return;

  }

 

  if (currentMenuState != MAIN_DISPLAY) {

    displayMenu();

    return;

  }

 

  // ===== TAMPILAN UTAMA HANYA JAM & TANGGAL =====

  lcd.clear();

 

  // Baris 1: Jam (format HH:MM:SS) - rata tengah

  String timeStr = formatTime(currentHour, currentMinute, currentSecond);

  int timePadding = (16 - timeStr.length()) / 2;

  lcd.setCursor(timePadding, 0);

  lcd.print(timeStr);

 

  // Baris 2: Tanggal (format Hari, DD Bulan YYYY) - rata tengah

  String dateStr = formatDate(currentDate, currentMonth, currentYear, currentDay);

  int datePadding = (16 - dateStr.length()) / 2;

  lcd.setCursor(datePadding, 1);

  lcd.print(dateStr);

}

 

// ===== MODIFIKASI: Display Menu dengan penyederhanaan =====

void displayMenu() {

  lcd.clear();

 

  switch (currentMenuState) {

    case MAIN_DISPLAY:

      // Akan dihandle oleh updateLCDDisplay()

      break;

     

    case MENU_MAIN:

      lcd.setCursor(0, 0);

      lcd.print("== MENU UTAMA ==");

      lcd.setCursor(0, 1);

      switch (menuPosition) {

        case 0: lcd.print("> Set Waktu"); break;

        case 1: lcd.print("> Set Tanggal"); break;

        case 2: lcd.print("> Lihat Jadwal"); break;

        case 3: lcd.print("> Test Bell"); break;

        case 4: lcd.print("> Info Sistem"); break;

        case 5: lcd.print("> Setting RTC"); break;

        case 6: lcd.print("> Backup/Restore"); break;

        case 7: lcd.print("> Power Setting"); break;

        case 8: lcd.print("> Jadwal Hari Ini"); break;

      }

      break;

     

    case MENU_SELECT_DAY:

      displayDaySelection();

      break;

     

    case MENU_VIEW_SCHEDULE:

      displayScheduleForDay();

      break;

     

    case MENU_SET_TIME:

      lcd.setCursor(0, 0);

      lcd.print("SET WAKTU");

      lcd.setCursor(0, 1);

      if (editing && cursorPosition == 0 && blinkState) {

        lcd.print("  :");

        lcd.print(currentMinute < 10 ? "0" : "");

        lcd.print(currentMinute);

      } else if (editing && cursorPosition == 1 && blinkState) {

        lcd.print(currentHour < 10 ? "0" : "");

        lcd.print(currentHour);

        lcd.print(":  ");

      } else {

        lcd.print(currentHour < 10 ? "0" : "");

        lcd.print(currentHour);

        lcd.print(":");

        lcd.print(currentMinute < 10 ? "0" : "");

        lcd.print(currentMinute);

      }

      break;

     

    case MENU_SET_DATE:

      lcd.setCursor(0, 0);

      lcd.print("SET TANGGAL");

      lcd.setCursor(0, 1);

      if (editing) {

        if (cursorPosition == 0 && blinkState) {

          lcd.print("  /");

          lcd.print(currentMonth < 10 ? "0" : "");

          lcd.print(currentMonth);

          lcd.print("/");

          lcd.print(currentYear);

        } else if (cursorPosition == 1 && blinkState) {

          lcd.print(currentDate < 10 ? "0" : "");

          lcd.print(currentDate);

          lcd.print("/  /");

          lcd.print(currentYear);

        } else if (cursorPosition == 2 && blinkState) {

          lcd.print(currentDate < 10 ? "0" : "");

          lcd.print(currentDate);

          lcd.print("/");

          lcd.print(currentMonth < 10 ? "0" : "");

          lcd.print(currentMonth);

          lcd.print("/    ");

        } else {

          lcd.print(currentDate < 10 ? "0" : "");

          lcd.print(currentDate);

          lcd.print("/");

          lcd.print(currentMonth < 10 ? "0" : "");

          lcd.print(currentMonth);

          lcd.print("/");

          lcd.print(currentYear);

        }

      } else {

        lcd.print(currentDate < 10 ? "0" : "");

        lcd.print(currentDate);

        lcd.print("/");

        lcd.print(currentMonth < 10 ? "0" : "");

        lcd.print(currentMonth);

        lcd.print("/");

        lcd.print(currentYear);

      }

      break;

     

    case MENU_SYSTEM_INFO:

      lcd.setCursor(0, 0);

      lcd.print("WiFi:");

      lcd.print(wifiConnected ? "OK" : "NO");

      lcd.print(" RTC:");

      lcd.print(rtcFound ? "OK" : "NO");

      lcd.setCursor(0, 1);

      lcd.print("Mem:");

      lcd.print(ESP.getFreeHeap() / 1024);

      lcd.print("KB Pwr:");

      lcd.print(powerSavingMode ? "ON" : "OFF");

      break;

     

    case MENU_RTC_SETTINGS:

      lcd.setCursor(0, 0);

      lcd.print("SETTING RTC");

      lcd.setCursor(0, 1);

      switch (menuPosition) {

        case 0: lcd.print("> Sync RTC-NTP"); break;

        case 1: lcd.print("> Deteksi RTC"); break;

        case 2: lcd.print("> AutoSync:ON/OFF"); break;

        case 3: lcd.print("> Kembali"); break;

      }

      break;

     

    case MENU_BACKUP_RESTORE:

      lcd.setCursor(0, 0);

      lcd.print("BACKUP/RESTORE");

      lcd.setCursor(0, 1);

      switch (menuPosition) {

        case 0: lcd.print("> Backup System"); break;

        case 1: lcd.print("> Restore System"); break;

        case 2: lcd.print("> Kembali"); break;

      }

      break;

     

    case MENU_POWER_SETTINGS:

      lcd.setCursor(0, 0);

      lcd.print("POWER SETTINGS");

      lcd.setCursor(0, 1);

      switch (menuPosition) {

        case 0: lcd.print("> Power Saving"); break;

        case 1: lcd.print("> Toggle Backlight"); break;

        case 2: lcd.print("> Kembali"); break;

      }

      break;

 

    case MENU_TEST_BELL:

      lcd.setCursor(0, 0);

      lcd.print("TEST BELL");

      lcd.setCursor(0, 1);

      lcd.print("Putar track 1");

      break;

  }

}

 

// ===== MODIFIKASI: Handle Buttons - Tombol untuk akses menu =====

void handleButtons() {

  // Update waktu aktivitas terakhir untuk power saving

  lastActivityTime = millis();

 

  // Hidupkan backlight jika mati

  if (!lcdBacklightOn) {

    lcd.backlight();

    lcdBacklightOn = true;

  }

 

  int upState = digitalRead(btnUp);

  int downState = digitalRead(btnDown);

  int okState = digitalRead(btnOk);

  int backState = digitalRead(btnBack);

 

  // Debounce

  if ((millis() - lastDebounceTime) > debounceDelay) {

   

    if (upState == LOW) {

      Serial.println("Tombol UP ditekan");

      logSystemEvent("Tombol UP ditekan");

     

      if (systemLocked) {

        addNotification("System terkunci", 2);

        return;

      }

     

      if (currentMenuState == MAIN_DISPLAY) {

        // Di main display, tombol UP langsung masuk ke menu utama

        currentMenuState = MENU_MAIN;

        menuPosition = 0;

      } else if (editing) {

        // Editing mode - increment value

        if (currentMenuState == MENU_SET_TIME) {

          if (cursorPosition == 0) {

            currentHour = (currentHour + 1) % 24;

          } else if (cursorPosition == 1) {

            currentMinute = (currentMinute + 1) % 60;

          }

        } else if (currentMenuState == MENU_SET_DATE) {

          if (cursorPosition == 0) {

            currentDate = (currentDate % 31) + 1;

          } else if (cursorPosition == 1) {

            currentMonth = (currentMonth % 12) + 1;

          } else if (cursorPosition == 2) {

            currentYear++;

            if (currentYear > 2030) currentYear = 2023;

          }

        }

      } else {

        // Navigation mode

        menuPosition--;

        if (menuPosition < 0) {

          if (currentMenuState == MENU_MAIN) menuPosition = 8;

          else if (currentMenuState == MENU_SET_TIME) menuPosition = 1;

          else if (currentMenuState == MENU_SET_DATE) menuPosition = 2;

          else if (currentMenuState == MENU_RTC_SETTINGS) menuPosition = 3;

          else if (currentMenuState == MENU_BACKUP_RESTORE) menuPosition = 2;

          else if (currentMenuState == MENU_POWER_SETTINGS) menuPosition = 2;

          else if (currentMenuState == MENU_SELECT_DAY) menuPosition = 6;

          else if (currentMenuState == MENU_VIEW_SCHEDULE) {

            // Untuk view schedule, decrement page

            int maxPages = (totalBells[selectedDay] + SCHEDULE_PER_PAGE - 1) / SCHEDULE_PER_PAGE;

            schedulePage = (schedulePage - 1 + maxPages) % maxPages;

          }

        }

      }

      lastDebounceTime = millis();

    }

   

    else if (downState == LOW) {

      Serial.println("Tombol DOWN ditekan");

      logSystemEvent("Tombol DOWN ditekan");

     

      if (systemLocked) {

        addNotification("System terkunci", 2);

        return;

      }

     

      if (currentMenuState == MAIN_DISPLAY) {

        // Di main display, tombol DOWN langsung test bell

        testBellNow();

      } else if (editing) {

        // Editing mode - decrement value

        if (currentMenuState == MENU_SET_TIME) {

          if (cursorPosition == 0) {

            currentHour = (currentHour - 1 + 24) % 24;

          } else if (cursorPosition == 1) {

            currentMinute = (currentMinute - 1 + 60) % 60;

          }

        } else if (currentMenuState == MENU_SET_DATE) {

          if (cursorPosition == 0) {

            currentDate = (currentDate - 2 + 31) % 31 + 1;

          } else if (cursorPosition == 1) {

            currentMonth = (currentMonth - 2 + 12) % 12 + 1;

          } else if (cursorPosition == 2) {

            currentYear--;

            if (currentYear < 2023) currentYear = 2030;

          }

        }

      } else {

        // Navigation mode

        menuPosition++;

        if (currentMenuState == MENU_MAIN && menuPosition > 8) menuPosition = 0;

        else if (currentMenuState == MENU_SET_TIME && menuPosition > 1) menuPosition = 0;

        else if (currentMenuState == MENU_SET_DATE && menuPosition > 2) menuPosition = 0;

        else if (currentMenuState == MENU_RTC_SETTINGS && menuPosition > 3) menuPosition = 0;

        else if (currentMenuState == MENU_BACKUP_RESTORE && menuPosition > 2) menuPosition = 0;

        else if (currentMenuState == MENU_POWER_SETTINGS && menuPosition > 2) menuPosition = 0;

        else if (currentMenuState == MENU_SELECT_DAY && menuPosition > 6) menuPosition = 0;

        else if (currentMenuState == MENU_VIEW_SCHEDULE) {

          // Untuk view schedule, increment page

          int maxPages = (totalBells[selectedDay] + SCHEDULE_PER_PAGE - 1) / SCHEDULE_PER_PAGE;

          schedulePage = (schedulePage + 1) % maxPages;

        }

      }

      lastDebounceTime = millis();

    }

   

    else if (okState == LOW) {

      Serial.println("Tombol OK ditekan");

      logSystemEvent("Tombol OK ditekan");

     

      if (systemLocked) {

        // Coba recover system dengan long press

        static unsigned long okPressTime = 0;

        if (okPressTime == 0) {

          okPressTime = millis();

        } else if (millis() - okPressTime > 3000) {

          recoverSystem();

          okPressTime = 0;

        }

        return;

      }

     

      if (currentMenuState == MAIN_DISPLAY) {

        // Di main display, tombol OK masuk ke menu utama

        currentMenuState = MENU_MAIN;

        menuPosition = 0;

      } else if (editing) {

        // Keluar dari editing mode

        editing = false;

        cursorPosition = 0;

        // Simpan perubahan waktu/date ke RTC

        saveRTCTime(currentHour, currentMinute, currentDate, currentMonth, currentYear);

       

        lcd.clear();

        lcd.setCursor(0, 0);

        lcd.print("Waktu Disimpan");

        lcd.setCursor(0, 1);

        lcd.print("Ke RTC " + rtcType);

        delay(2000);

        currentMenuState = MAIN_DISPLAY; // Kembali ke tampilan utama

      } else {

        // Masuk ke submenu atau action

        switch (currentMenuState) {

          case MENU_MAIN:

            switch (menuPosition) {

              case 0: currentMenuState = MENU_SET_TIME; menuPosition = 0; break;

              case 1: currentMenuState = MENU_SET_DATE; menuPosition = 0; break;

              case 2: currentMenuState = MENU_SELECT_DAY; menuPosition = 0; break;

              case 3:

                testBellNow();

                currentMenuState = MAIN_DISPLAY; // Kembali ke tampilan utama setelah test

                break;

              case 4: currentMenuState = MENU_SYSTEM_INFO; break;

              case 5: currentMenuState = MENU_RTC_SETTINGS; menuPosition = 0; break;

              case 6: currentMenuState = MENU_BACKUP_RESTORE; menuPosition = 0; break;

              case 7: currentMenuState = MENU_POWER_SETTINGS; menuPosition = 0; break;

              case 8:

                currentMenuState = MENU_VIEW_SCHEDULE;

                selectedDay = currentDay;

                schedulePage = 0;

                break;

            }

            break;

          case MENU_SET_TIME:

          case MENU_SET_DATE:

            editing = true;

            cursorPosition = menuPosition;

            break;

          case MENU_TEST_BELL:

            testBellNow();

            currentMenuState = MAIN_DISPLAY; // Kembali ke tampilan utama

            break;

          case MENU_SELECT_DAY:

            selectedDay = menuPosition;

            currentMenuState = MENU_VIEW_SCHEDULE;

            schedulePage = 0;

            break;

          case MENU_VIEW_SCHEDULE:

            // Kembali ke menu select day

            currentMenuState = MENU_SELECT_DAY;

            break;

          case MENU_SYSTEM_INFO:

            // Kembali ke menu utama setelah melihat info

            currentMenuState = MENU_MAIN;

            break;

          case MENU_RTC_SETTINGS:

            switch (menuPosition) {

              case 0: // Sync RTC dengan NTP

                if (wifiConnected && rtcFound) {

                  syncRTCWithNTP();

                  lcd.clear();

                  lcd.setCursor(0, 0);

                  lcd.print("RTC Disinkronisasi");

                  lcd.setCursor(0, 1);

                  lcd.print("Dengan NTP");

                  delay(2000);

                } else {

                  lcd.clear();

                  lcd.setCursor(0, 0);

                  lcd.print("Tidak bisa sync");

                  lcd.setCursor(0, 1);

                  lcd.print("Cek WiFi/RTC");

                  delay(2000);

                }

                currentMenuState = MAIN_DISPLAY; // Kembali ke tampilan utama

                break;

              case 1: // Deteksi ulang RTC

                detectRTC();

                lcd.clear();

                lcd.setCursor(0, 0);

                lcd.print("Deteksi RTC");

                lcd.setCursor(0, 1);

                lcd.print(rtcType);

                delay(2000);

                currentMenuState = MAIN_DISPLAY; // Kembali ke tampilan utama

                break;

              case 2: // Toggle Auto-Sync

                autoSyncEnabled = !autoSyncEnabled;

                lcd.clear();

                lcd.setCursor(0, 0);

                lcd.print("Auto-Sync:");

                lcd.setCursor(0, 1);

                lcd.print(autoSyncEnabled ? "AKTIF" : "NON-AKTIF");

                delay(2000);

                currentMenuState = MAIN_DISPLAY; // Kembali ke tampilan utama

                break;

              case 3: // Kembali

                currentMenuState = MENU_MAIN;

                menuPosition = 5;

                break;

            }

            break;

          case MENU_BACKUP_RESTORE:

            switch (menuPosition) {

              case 0: // Backup System

                backupSystemConfig();

                lcd.clear();

                lcd.setCursor(0, 0);

                lcd.print("Backup Berhasil");

                lcd.setCursor(0, 1);

                lcd.print(lastBackupTime);

                delay(2000);

                currentMenuState = MAIN_DISPLAY; // Kembali ke tampilan utama

                break;

              case 1: // Restore System

                restoreSystemConfig();

                lcd.clear();

                lcd.setCursor(0, 0);

                lcd.print("Restore Berhasil");

                lcd.setCursor(0, 1);

                lcd.print(lastRestoreTime);

                delay(2000);

                currentMenuState = MAIN_DISPLAY; // Kembali ke tampilan utama

                break;

              case 2: // Kembali

                currentMenuState = MENU_MAIN;

                menuPosition = 6;

                break;

            }

            break;

          case MENU_POWER_SETTINGS:

            switch (menuPosition) {

              case 0: // Toggle Power Saving

                powerSavingMode = !powerSavingMode;

                lcd.clear();

                lcd.setCursor(0, 0);

                lcd.print("Power Saving:");

                lcd.setCursor(0, 1);

                lcd.print(powerSavingMode ? "AKTIF" : "NON-AKTIF");

                delay(2000);

                currentMenuState = MAIN_DISPLAY; // Kembali ke tampilan utama

                break;

              case 1: // Toggle Backlight

                toggleBacklight();

                lcd.clear();

                lcd.setCursor(0, 0);

                lcd.print("Backlight:");

                lcd.setCursor(0, 1);

                lcd.print(lcdBacklightOn ? "HIDUP" : "MATI");

                delay(2000);

                currentMenuState = MAIN_DISPLAY; // Kembali ke tampilan utama

                break;

              case 2: // Kembali

                currentMenuState = MENU_MAIN;

                menuPosition = 7;

                break;

            }

            break;

        }

      }

      lastDebounceTime = millis();

    }

   

    else if (backState == LOW) {

      Serial.println("Tombol BACK ditekan");

      logSystemEvent("Tombol BACK ditekan");

     

      if (systemLocked) {

        return;

      }

     

      if (editing) {

        editing = false;

        cursorPosition = 0;

        // Batalkan perubahan, baca ulang dari RTC

        readTimeFromRTC();

       

        lcd.clear();

        lcd.setCursor(0, 0);

        lcd.print("Perubahan");

        lcd.setCursor(0, 1);

        lcd.print("Dibatalkan");

        delay(2000);

        currentMenuState = MAIN_DISPLAY; // Kembali ke tampilan utama

      } else {

        switch (currentMenuState) {

          case MENU_MAIN:

            currentMenuState = MAIN_DISPLAY;

            break;

          case MENU_SELECT_DAY:

            currentMenuState = MENU_MAIN;

            menuPosition = 2;

            break;

          case MENU_VIEW_SCHEDULE:

            currentMenuState = MENU_SELECT_DAY;

            break;

          case MENU_SYSTEM_INFO:

            currentMenuState = MENU_MAIN;

            menuPosition = 4;

            break;

          default:

            currentMenuState = MAIN_DISPLAY; // Default kembali ke tampilan utama

            break;

        }

      }

      lastDebounceTime = millis();

    }

  }

}

 

// ===== MODIFIKASI: Web Interface untuk jadwal terpisah =====

String getDayScheduleHTML(int day) {

  String html = "";

  Bell* daySchedule = schedulePointers[day];

 

  for(int i = 0; i < totalBells[day]; i++) {

    Bell b = daySchedule[i];

    html += "<tr>";

    html += "<td data-label='Hari'>" + daysOfWeek[day] + "</td>";

    html += "<td data-label='Jam'><input type='number' value='" + String(b.hour) + "' onchange='edit(" + String(day) + "," + String(i) + ",\"hour\",this.value)'></td>";

    html += "<td data-label='Menit'><input type='number' value='" + String(b.minute) + "' onchange='edit(" + String(day) + "," + String(i) + ",\"minute\",this.value)'></td>";

    html += "<td data-label='Track'><input type='number' value='" + String(b.track) + "' onchange='edit(" + String(day) + "," + String(i) + ",\"track\",this.value)'></td>";

    html += "<td data-label='Durasi'><input type='number' value='" + String(b.duration) + "' onchange='edit(" + String(day) + "," + String(i) + ",\"duration\",this.value)'></td>";

    html += "<td data-label='Keterangan'><input type='text' class='desc' value='" + b.description + "' onchange='edit(" + String(day) + "," + String(i) + ",\"desc\",this.value)'></td>";

    html += "<td data-label='Aktif'><input type='checkbox' " + String(b.enabled ? "checked" : "") + " onchange='edit(" + String(day) + "," + String(i) + ",\"enabled\",this.checked?'1':'0')'></td>";

    html += "<td data-label='Aksi'>";

    html += "<button onclick='deleteBell(" + String(day) + "," + String(i) + ")' class='btn-delete'>Hapus</button>";

    html += "<button onclick='test(" + String(b.track) + ")' class='btn-test'>Test</button>";

    html += "</td>";

    html += "</tr>";

  }

 

  return html;

}

 

String getHTML() {

  String html = R"rawliteral(

<!DOCTYPE html>

<html lang="id">

<head>

  <meta charset="UTF-8">

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

  <title>Sistem Bel 24 Jam - Jadwal Terpisah</title>

  <style>

    body { font-family: Arial, sans-serif; margin: 20px; background: #f5f5f5; }

    .container { max-width: 1400px; margin: 0 auto; background: white; padding: 20px; border-radius: 10px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }

    h2 { color: #007bff; border-bottom: 2px solid #007bff; padding-bottom: 10px; }

    table { width: 100%; border-collapse: collapse; margin: 20px 0; }

    th, td { border: 1px solid #ddd; padding: 12px; text-align: center; }

    th { background: #007bff; color: white; }

    tr:nth-child(even) { background: #f9f9f9; }

    input[type="number"], input[type="text"] { width: 80px; padding: 5px; border: 1px solid #ddd; border-radius: 4px; }

    input[type="text"].desc { width: 150px; }

    button { padding: 8px 15px; margin: 2px; border: none; border-radius: 4px; cursor: pointer; }

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

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

    .btn-edit { background: #ffc107; color: black; }

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

    .btn-warning { background: #fd7e14; color: white; }

    .btn-info { background: #17a2b8; color: white; }

    .status { padding: 10px; margin: 10px 0; border-radius: 5px; }

    .status-online { background: #d4edda; color: #155724; }

    .status-offline { background: #f8d7da; color: #721c24; }

    .status-warning { background: #fff3cd; color: #856404; }

    .uptime { background: #e7f3ff; padding: 10px; margin: 10px 0; border-radius: 5px; border-left: 4px solid #007bff; }

    .controls { background: #e9ecef; padding: 15px; border-radius: 5px; margin: 20px 0; }

    .health-stats { background: #d1ecf1; padding: 15px; border-radius: 5px; margin: 20px 0; border-left: 4px solid #17a2b8; }

    .notifications { background: #fff3cd; padding: 15px; border-radius: 5px; margin: 20px 0; border-left: 4px solid #ffc107; }

    .logs { background: #f8f9fa; padding: 15px; border-radius: 5px; margin: 20px 0; border-left: 4px solid #6c757d; }

    .day-tabs { display: flex; margin: 20px 0; border-bottom: 2px solid #007bff; }

    .day-tab { padding: 10px 20px; background: #f8f9fa; border: 1px solid #ddd; border-bottom: none; margin-right: 5px; cursor: pointer; border-radius: 5px 5px 0 0; }

    .day-tab.active { background: #007bff; color: white; }

    .day-tab:hover { background: #e9ecef; }

    .day-content { display: none; }

    .day-content.active { display: block; }

    .emergency { background: #f8d7da; padding: 15px; border-radius: 5px; margin: 20px 0; border-left: 4px solid #dc3545; animation: blink 1s infinite; }

    @keyframes blink { 50% { opacity: 0.7; } }

    @media (max-width: 768px) {

      table, thead, tbody, th, td, tr { display: block; }

      th { display: none; }

      td { border: none; border-bottom: 1px solid #ddd; position: relative; padding-left: 50%; }

      td:before { position: absolute; left: 10px; width: 45%; white-space: nowrap; font-weight: bold; }

      td:nth-of-type(1):before { content: "Hari"; }

      td:nth-of-type(2):before { content: "Jam"; }

      td:nth-of-type(3):before { content: "Menit"; }

      td:nth-of-type(4):before { content: "Track"; }

      td:nth-of-type(5):before { content: "Durasi"; }

      td:nth-of-type(6):before { content: "Keterangan"; }

      td:nth-of-type(7):before { content: "Aktif"; }

      td:nth-of-type(8):before { content: "Aksi"; }

      .day-tabs { flex-wrap: wrap; }

      .day-tab { margin-bottom: 5px; }

    }

  </style>

</head>

<body>

  <div class="container">

    <h2>📅 Sistem Bel 24 Jam - Jadwal Terpisah Per Hari</h2>

   

    )rawliteral";

   

    // Tambahkan emergency warning jika dalam mode emergency

    if (emergencyMode) {

      html += R"rawliteral(

    <div class="emergency">

      <strong>🚨 MODE DARURAT AKTIF!</strong><br>

      <strong>Alasan:</strong> %EMERGENCY_REASON%<br>

      <strong>Dimulai:</strong> %EMERGENCY_TIME%<br>

      <button onclick="recoverSystem()" class="btn-save">Pulihkan Sistem</button>

    </div>

      )rawliteral";

    }

   

    html += R"rawliteral(

   

    <div class="status %STATUS_CLASS%">

      <strong>Status Sistem:</strong> WiFi: %WIFI_STATUS% | DFPlayer: %DFPLAYER_STATUS% | RTC: %RTC_STATUS% (%RTC_TYPE%)<br>

      <strong>Waktu Saat Ini:</strong> %CURRENT_TIME% | %CURRENT_DATE%<br>

      <strong>Volume:</strong> %VOLUME% | <strong>Auto-Sync:</strong> %AUTO_SYNC% | <strong>Bell Berikutnya:</strong> %NEXT_BELL%

    </div>

   

    <div class="uptime">

      <strong> Durasi Jam Lama (Uptime):</strong> %UPTIME% | <strong>Mode:</strong> %POWER_MODE% | <strong>Memory:</strong> %MEMORY_USAGE%KB

    </div>

   

    <div class="health-stats">

      <h3>📊 Health Monitoring</h3>

      <strong>WiFi Strength:</strong> %WIFI_STRENGTH% dBm |

      <strong>Notifikasi:</strong> %NOTIFICATION_COUNT% |

      <strong>Log Entries:</strong> %LOG_COUNT% |

      <strong>Backup Terakhir:</strong> %LAST_BACKUP% |

      <button onclick="performHealthCheck()" class="btn-edit">Refresh Health</button>

    </div>

   

    )rawliteral";

   

    // Tampilkan notifikasi jika ada

    if (notificationCount > 0) {

      html += R"rawliteral(

    <div class="notifications">

      <h3>🔔 Notifikasi Sistem (%NOTIFICATION_COUNT%)</h3>

      )rawliteral";

     

      for (int i = notificationCount - 1; i >= 0 && i >= notificationCount - 5; i--) {

        String priorityIcon = "";

        if (notifications[i].priority == 3) priorityIcon = "🔴 ";

        else if (notifications[i].priority == 2) priorityIcon = "🟡 ";

        else priorityIcon = "🔵 ";

       

        html += "<div>" + priorityIcon + notifications[i].message + "</div>";

      }

     

      html += R"rawliteral(

      <button onclick="clearNotifications()" class="btn-edit">Bersihkan Notifikasi</button>

    </div>

      )rawliteral";

    }

   

    html += R"rawliteral(

   

    <div class="controls">

      <h3>🎛 Kontrol Sistem Lanjutan</h3>

      <button onclick="syncTime()" class="btn-save">Sinkronisasi Waktu NTP</button>

      <button onclick="updateRTC()" class="btn-save">Update RTC</button>

      <button onclick="toggleAutoSync()" class="btn-edit">Toggle Auto-Sync</button>

      <button onclick="resetBells()" class="btn-edit">Reset Status Bell</button>

      <button onclick="backupSystem()" class="btn-save">Backup System</button>

      <button onclick="restoreSystem()" class="btn-warning">Restore System</button>

      <button onclick="togglePowerSaving()" class="btn-edit">Toggle Power Saving</button>

      <br><br>

      Volume: <input type="number" id="volume" min="0" max="30" value="%VOLUME%">

      <button onclick="setVolume()" class="btn-save">Set Volume</button>

     

      <div style="margin-top: 15px;">

        <h4>Manajemen Jadwal Cepat:</h4>

        <button onclick="setDefaultSchedule(1)" class="btn-info">Jadwal Default Senin</button>

        <button onclick="setDefaultSchedule(2)" class="btn-info">Jadwal Default Selasa</button>

        <button onclick="setDefaultSchedule(3)" class="btn-info">Jadwal Default Rabu</button>

        <button onclick="setDefaultSchedule(4)" class="btn-info">Jadwal Default Kamis</button>

        <button onclick="setDefaultSchedule(5)" class="btn-info">Jadwal Default Jumat</button>

        <button onclick="setDefaultSchedule(6)" class="btn-info">Jadwal Default Sabtu</button>

        <button onclick="setDefaultSchedule(0)" class="btn-info">Jadwal Default Minggu</button>

      </div>

    </div>

   

    )rawliteral";

   

    // Tambahkan system logs

    html += getSystemLogsHTML();

   

    html += R"rawliteral(

   

    <div class="day-tabs" id="dayTabs">

      <div class="day-tab active" onclick="showDay(0)">Minggu</div>

      <div class="day-tab" onclick="showDay(1)">Senin</div>

      <div class="day-tab" onclick="showDay(2)">Selasa</div>

      <div class="day-tab" onclick="showDay(3)">Rabu</div>

      <div class="day-tab" onclick="showDay(4)">Kamis</div>

      <div class="day-tab" onclick="showDay(5)">Jumat</div>

      <div class="day-tab" onclick="showDay(6)">Sabtu</div>

    </div>

   

    <div id="dayContents">

      )rawliteral";

     

      // Generate content untuk setiap hari

      for (int day = 0; day < 7; day++) {

        String activeClass = (day == 0) ? "active" : "";

        html += "<div class='day-content " + activeClass + "' id='dayContent" + String(day) + "'>";

        html += "<h3>Jadwal " + daysOfWeek[day] + " (" + String(totalBells[day]) + " jadwal)</h3>";

       

        html += "<div style='margin: 10px 0;'>";

        html += "<button onclick=\"copySchedule(" + String(day) + ")\" class='btn-info'>Salin Jadwal Hari Ini</button>";

        html += "<button onclick=\"clearDaySchedule(" + String(day) + ")\" class='btn-warning'>Kosongkan Jadwal</button>";

        html += "</div>";

       

        html += "<table>";

        html += "<thead><tr><th>Hari</th><th>Jam</th><th>Menit</th><th>Track</th><th>Durasi</th><th>Keterangan</th><th>Aktif</th><th>Aksi</th></tr></thead>";

        html += "<tbody>";

        html += getDayScheduleHTML(day);

        html += "</tbody></table>";

       

        html += "<div class='controls'>";

        html += "<h4>Tambah Jadwal untuk " + daysOfWeek[day] + "</h4>";

        html += "Jam: <input id='newHour" + String(day) + "' type='number' min='0' max='23' placeholder='Jam'>";

        html += "Menit: <input id='newMin" + String(day) + "' type='number' min='0' max='59' placeholder='Menit'>";

        html += "Track: <input id='newTrack" + String(day) + "' type='number' min='1' max='3000' placeholder='Track'>";

        html += "Durasi: <input id='newDur" + String(day) + "' type='number' min='1' max='300' placeholder='Detik'>";

        html += "Keterangan: <input id='newDesc" + String(day) + "' type='text' class='desc' placeholder='Keterangan'>";

        html += "<button onclick=\"addNewForDay(" + String(day) + ")\" class='btn-save'>Tambah Jadwal</button>";

        html += "</div>";

       

        html += "</div>";

      }

     

      html += R"rawliteral(

    </div>

  </div>

 

  <script>

    let currentDay = 0;

   

    function showDay(day) {

      // Update tabs

      document.querySelectorAll('.day-tab').forEach(tab => tab.classList.remove('active'));

      document.querySelectorAll('.day-tab')[day].classList.add('active');

     

      // Update contents

      document.querySelectorAll('.day-content').forEach(content => content.classList.remove('active'));

      document.getElementById('dayContent' + day).classList.add('active');

     

      currentDay = day;

    }

   

    function edit(day, i, field, value) {

      fetch('/edit?day=' + day + '&index=' + i + '&field=' + field + '&val=' + encodeURIComponent(value))

        .then(() => location.reload());

    }

    

    function deleteBell(day, i) {

      if(confirm('Hapus jadwal ini?')) {

        fetch('/delete?day=' + day + '&index=' + i)

          .then(() => location.reload());

      }

    }

   

    function addNewForDay(day) {

      let hour = document.getElementById('newHour' + day).value;

      let minute = document.getElementById('newMin' + day).value;

      let track = document.getElementById('newTrack' + day).value;

      let duration = document.getElementById('newDur' + day).value;

      let desc = document.getElementById('newDesc' + day).value;

     

      if(hour && minute && track && duration && desc) {

        fetch('/add?day=' + day + '&hour=' + hour + '&minute=' + minute + '&track=' + track + '&duration=' + duration + '&description=' + encodeURIComponent(desc))

          .then(() => location.reload());

      } else {

        alert('Harap isi semua field!');

      }

    }

   

    function test(track) {

      fetch('/test?track=' + track);

    }

   

    function syncTime() {

      fetch('/syncTime').then(() => {

        alert('Waktu disinkronisasi dengan NTP');

        location.reload();

      });

    }

   

    function updateRTC() {

      fetch('/updateRTC').then(() => {

        alert('RTC diperbarui dari NTP');

        location.reload();

      });

    }

   

    function toggleAutoSync() {

      fetch('/toggleAutoSync').then(() => {

        alert('Auto-Sync di-toggle');

        location.reload();

      });

    }

   

    function resetBells() {

      fetch('/resetBells').then(() => {

        alert('Status bell direset');

        location.reload();

      });

    }

   

    function setVolume() {

      let vol = document.getElementById('volume').value;

      fetch('/setVolume?volume=' + vol).then(() => {

        alert('Volume diatur ke ' + vol);

      });

    }

   

    function backupSystem() {

      if(confirm('Backup konfigurasi sistem?')) {

        fetch('/backupSystem').then(() => {

          alert('Backup sistem berhasil');

          location.reload();

        });

      }

    }

   

    function restoreSystem() {

      if(confirm('Restore konfigurasi sistem? Semua pengaturan saat ini akan diganti.')) {

        fetch('/restoreSystem').then(() => {

          alert('Restore sistem berhasil');

          location.reload();

        });

      }

    }

   

    function togglePowerSaving() {

      fetch('/togglePowerSaving').then(() => {

        alert('Power Saving di-toggle');

        location.reload();

      });

    }

   

    function performHealthCheck() {

      fetch('/healthCheck').then(() => {

        alert('Health check dilakukan');

        location.reload();

      });

    }

   

    function clearNotifications() {

      fetch('/clearNotifications').then(() => {

        alert('Notifikasi dibersihkan');

        location.reload();

      });

    }

   

    function recoverSystem() {

      if(confirm('Pulihkan sistem dari mode darurat?')) {

        fetch('/recoverSystem').then(() => {

          alert('Sistem berhasil dipulihkan');

          location.reload();

        });

      }

    }

   

    function setDefaultSchedule(day) {

      if(confirm('Set jadwal default untuk ' + getDayName(day) + '? Jadwal saat ini akan diganti.')) {

        fetch('/setDefaultSchedule?day=' + day).then(() => {

          alert('Jadwal default untuk ' + getDayName(day) + ' diset');

          location.reload();

        });

      }

    }

   

    function copySchedule(day) {

      let targetDay = prompt('Salin jadwal ' + getDayName(day) + ' ke hari apa? (0=Minggu, 1=Senin, ..., 6=Sabtu)');

      if(targetDay !== null && targetDay >= 0 && targetDay <= 6) {

        if(confirm('Salin jadwal ' + getDayName(day) + ' ke ' + getDayName(targetDay) + '?')) {

          fetch('/copySchedule?from=' + day + '&to=' + targetDay).then(() => {

            alert('Jadwal berhasil disalin');

            location.reload();

          });

        }

      }

    }

   

    function clearDaySchedule(day) {

      if(confirm('Kosongkan semua jadwal untuk ' + getDayName(day) + '?')) {

        fetch('/clearDaySchedule?day=' + day).then(() => {

          alert('Jadwal ' + getDayName(day) + ' dikosongkan');

          location.reload();

        });

      }

    }

   

    function getDayName(day) {

      const days = ['Minggu', 'Senin', 'Selasa', 'Rabu', 'Kamis', 'Jumat', 'Sabtu'];

      return days[day];

    }

   

    // Auto refresh setiap 30 detik

    setInterval(() => {

      location.reload();

    }, 30000);

  </script>

</body>

</html>

)rawliteral";

 

  // Replace placeholders

  String statusClass = (wifiConnected && dfPlayerReady && rtcFound) ? "status-online" : "status-offline";

  if (emergencyMode) statusClass = "status-warning";

 

  html.replace("%STATUS_CLASS%", statusClass);

  html.replace("%WIFI_STATUS%", wifiConnected ? "TERHUBUNG" : "TERPUTUS");

  html.replace("%DFPLAYER_STATUS%", dfPlayerReady ? "SIAP" : "GAGAL");

  html.replace("%RTC_STATUS%", rtcFound ? "SIAP" : "GAGAL");

  html.replace("%RTC_TYPE%", rtcType);

  html.replace("%CURRENT_TIME%", formatTime(currentHour, currentMinute, currentSecond));

  html.replace("%CURRENT_DATE%", formatDate(currentDate, currentMonth, currentYear, currentDay));

  html.replace("%VOLUME%", String(dfVolume));

  html.replace("%AUTO_SYNC%", autoSyncEnabled ? "AKTIF" : "NON-AKTIF");

  html.replace("%UPTIME%", formatUptime());

  html.replace("%POWER_MODE%", powerSavingMode ? "POWER SAVING" : "NORMAL");

  html.replace("%MEMORY_USAGE%", String(ESP.getFreeHeap() / 1024));

  html.replace("%WIFI_STRENGTH%", String(wifiStrength));

  html.replace("%NOTIFICATION_COUNT%", String(notificationCount));

  html.replace("%LOG_COUNT%", String(logCount));

  html.replace("%LAST_BACKUP%", lastBackupTime);

  html.replace("%EMERGENCY_REASON%", emergencyReason);

  html.replace("%EMERGENCY_TIME%", formatTime(currentHour, currentMinute) + " " + formatDate(currentDate, currentMonth, currentYear, currentDay));

 

  Bell nextBell = getNextBell(currentDay);

  String nextBellStr = (nextBell.hour != -1) ?

    (formatTime(nextBell.hour, nextBell.minute) + " - " + nextBell.description) : "Tidak ada";

  html.replace("%NEXT_BELL%", nextBellStr);

 

  return html;

}

 

// ===== Print System Status =====

void printSystemStatus() {

  Serial.println("\n=== STATUS SISTEM BEL 24 JAM ===");

  Serial.println("WiFi: " + String(wifiConnected ? "Terhubung" : "Terputus"));

  Serial.println("DFPlayer: " + String(dfPlayerReady ? "Siap" : "Gagal"));

  Serial.println("RTC: " + String(rtcFound ? rtcType : "Tidak ditemukan"));

  Serial.println("Auto-Sync: " + String(autoSyncEnabled ? "AKTIF" : "NON-AKTIF"));

  Serial.println("SPIFFS: " + String(spiffsMounted ? "Mounted" : "Gagal"));

  Serial.println("Waktu: " + formatTime(currentHour, currentMinute, currentSecond));

  Serial.println("Tanggal: " + formatDate(currentDate, currentMonth, currentYear, currentDay));

  Serial.println("Uptime: " + formatUptime());

  Serial.println("Volume: " + String(dfVolume));

  Serial.println("Power Saving: " + String(powerSavingMode ? "AKTIF" : "NON-AKTIF"));

  Serial.println("Notifikasi: " + String(notificationCount));

  Serial.println("================================\n");

}

 

// ===== MODIFIKASI: Setup Web Server dengan endpoint baru untuk jadwal terpisah =====

void setupWebServer() {

  server.on("/", HTTP_GET, [](AsyncWebServerRequest *req) {

    if(!req->authenticate(www_username, www_password)) {

      return req->requestAuthentication();

    }

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

  });

 

  server.on("/add", HTTP_GET, [](AsyncWebServerRequest *req) {

    if(!req->authenticate(www_username, www_password)) {

      return req->requestAuthentication();

    }

   

    int day = req->getParam("day")->value().toInt();

    int hour = req->getParam("hour")->value().toInt();

    int minute = req->getParam("minute")->value().toInt();

    int track = req->getParam("track")->value().toInt();

    int dur = req->getParam("duration")->value().toInt();

    String desc = req->getParam("description")->value();

   

    if(day >= 0 && day < 7 && hour >= 0 && hour < 24 && minute >= 0 && minute < 60) {

      addBell(day, hour, minute, track, dur, desc);

      saveSchedule();

    }

   

    req->redirect("/");

  });

 

  server.on("/edit", HTTP_GET, [](AsyncWebServerRequest *req) {

    if(!req->authenticate(www_username, www_password)) {

      return req->requestAuthentication();

    }

   

    int day = req->getParam("day")->value().toInt();

    int idx = req->getParam("index")->value().toInt();

    String field = req->getParam("field")->value();

    String val = req->getParam("val")->value();

   

    if(day >= 0 && day < 7 && idx >= 0 && idx < totalBells[day]) {

      Bell* daySchedule = schedulePointers[day];

      if(field == "hour") daySchedule[idx].hour = val.toInt();

      else if(field == "minute") daySchedule[idx].minute = val.toInt();

      else if(field == "track") daySchedule[idx].track = val.toInt();

      else if(field == "duration") daySchedule[idx].duration = val.toInt();

      else if(field == "desc") daySchedule[idx].description = val;

      else if(field == "enabled") daySchedule[idx].enabled = (val == "1");

     

      saveSchedule();

    }

    

    req->send(200, "text/plain", "OK");

  });

 

  server.on("/delete", HTTP_GET, [](AsyncWebServerRequest *req) {

    if(!req->authenticate(www_username, www_password)) {

      return req->requestAuthentication();

    }

   

    int day = req->getParam("day")->value().toInt();

    int idx = req->getParam("index")->value().toInt();

   

    if(day >= 0 && day < 7 && idx >= 0 && idx < totalBells[day]) {

      Bell* daySchedule = schedulePointers[day];

      for(int i = idx; i < totalBells[day] - 1; i++) {

        daySchedule[i] = daySchedule[i + 1];

      }

      totalBells[day]--;

      saveSchedule();

    }

   

    req->redirect("/");

  });

 

  server.on("/test", HTTP_GET, [](AsyncWebServerRequest *req) {

    if(!req->authenticate(www_username, www_password)) {

      return req->requestAuthentication();

    }

   

    int track = req->getParam("track")->value().toInt();

    if(dfPlayerReady && track > 0 && track <= 3000) {

      dfPlayer.play(track);

    }

   

    req->send(200, "text/plain", "OK");

  });

 

  server.on("/syncTime", HTTP_GET, [](AsyncWebServerRequest *req) {

    if(!req->authenticate(www_username, www_password)) {

      return req->requestAuthentication();

    }

   

    updateTimeFromNTP();

    req->send(200, "text/plain", "Waktu disinkronisasi");

  });

 

  server.on("/updateRTC", HTTP_GET, [](AsyncWebServerRequest *req) {

    if(!req->authenticate(www_username, www_password)) {

      return req->requestAuthentication();

    }

   

    if(rtcFound) {

      updateRTCFromNTP();

      req->send(200, "text/plain", "RTC diperbarui");

    } else {

      req->send(200, "text/plain", "RTC tidak tersedia");

    }

  });

 

  server.on("/toggleAutoSync", HTTP_GET, [](AsyncWebServerRequest *req) {

    if(!req->authenticate(www_username, www_password)) {

      return req->requestAuthentication();

    }

   

    autoSyncEnabled = !autoSyncEnabled;

    req->send(200, "text/plain", "Auto-Sync: " + String(autoSyncEnabled ? "AKTIF" : "NON-AKTIF"));

  });

 

  server.on("/resetBells", HTTP_GET, [](AsyncWebServerRequest *req) {

    if(!req->authenticate(www_username, www_password)) {

      return req->requestAuthentication();

    }

   

    resetDailyBells();

    req->send(200, "text/plain", "Status bell direset");

  });

 

  server.on("/setVolume", HTTP_GET, [](AsyncWebServerRequest *req) {

    if(!req->authenticate(www_username, www_password)) {

      return req->requestAuthentication();

    }

   

    int volume = req->getParam("volume")->value().toInt();

    if(volume >= 0 && volume <= 30) {

      dfVolume = volume;

      if(dfPlayerReady) dfPlayer.volume(dfVolume);

      saveSchedule();

    }

   

    req->send(200, "text/plain", "Volume diatur ke " + String(volume));

  });

 

  // ENDPOINT BARU UNTUK JADWAL TERPISAH

  server.on("/setDefaultSchedule", HTTP_GET, [](AsyncWebServerRequest *req) {

    if(!req->authenticate(www_username, www_password)) {

      return req->requestAuthentication();

    }

   

    int day = req->getParam("day")->value().toInt();

    if(day >= 0 && day < 7) {

      setDefaultScheduleForDay(day);

      saveSchedule();

    }

   

    req->send(200, "text/plain", "Jadwal default untuk " + daysOfWeek[day] + " diset");

  });

 

  server.on("/copySchedule", HTTP_GET, [](AsyncWebServerRequest *req) {

    if(!req->authenticate(www_username, www_password)) {

      return req->requestAuthentication();

    }

   

    int fromDay = req->getParam("from")->value().toInt();

    int toDay = req->getParam("to")->value().toInt();

   

    if(fromDay >= 0 && fromDay < 7 && toDay >= 0 && toDay < 7) {

      copySchedule(fromDay, toDay);

      saveSchedule();

    }

   

    req->send(200, "text/plain", "Jadwal disalin dari " + daysOfWeek[fromDay] + " ke " + daysOfWeek[toDay]);

  });

 

  server.on("/clearDaySchedule", HTTP_GET, [](AsyncWebServerRequest *req) {

    if(!req->authenticate(www_username, www_password)) {

      return req->requestAuthentication();

    }

   

    int day = req->getParam("day")->value().toInt();

    if(day >= 0 && day < 7) {

      clearDaySchedule(day);

      saveSchedule();

    }

   

    req->send(200, "text/plain", "Jadwal " + daysOfWeek[day] + " dikosongkan");

  });

 

  // ENDPOINT BARU

  server.on("/backupSystem", HTTP_GET, [](AsyncWebServerRequest *req) {

    if(!req->authenticate(www_username, www_password)) {

      return req->requestAuthentication();

    }

   

    backupSystemConfig();

    req->send(200, "text/plain", "Backup sistem berhasil");

  });

 

  server.on("/restoreSystem", HTTP_GET, [](AsyncWebServerRequest *req) {

    if(!req->authenticate(www_username, www_password)) {

      return req->requestAuthentication();

    }

   

    restoreSystemConfig();

    req->send(200, "text/plain", "Restore sistem berhasil");

  });

 

  server.on("/togglePowerSaving", HTTP_GET, [](AsyncWebServerRequest *req) {

    if(!req->authenticate(www_username, www_password)) {

      return req->requestAuthentication();

    }

   

    powerSavingMode = !powerSavingMode;

    req->send(200, "text/plain", "Power Saving: " + String(powerSavingMode ? "AKTIF" : "NON-AKTIF"));

  });

 

  server.on("/healthCheck", HTTP_GET, [](AsyncWebServerRequest *req) {

    if(!req->authenticate(www_username, www_password)) {

      return req->requestAuthentication();

    }

   

    performHealthCheck();

    req->send(200, "text/plain", "Health check dilakukan");

  });

 

  server.on("/clearNotifications", HTTP_GET, [](AsyncWebServerRequest *req) {

    if(!req->authenticate(www_username, www_password)) {

      return req->requestAuthentication();

    }

   

    clearNotifications();

    req->send(200, "text/plain", "Notifikasi dibersihkan");

  });

 

  server.on("/recoverSystem", HTTP_GET, [](AsyncWebServerRequest *req) {

    if(!req->authenticate(www_username, www_password)) {

      return req->requestAuthentication();

    }

   

    recoverSystem();

    req->send(200, "text/plain", "Sistem dipulihkan");

  });

 

  server.begin();

  Serial.println("Web server berjalan di http://" + WiFi.localIP().toString());

}

 

// ===== MODIFIKASI: Setup dengan inisialisasi struktur baru =====

void setup() {

  Serial.begin(115200);

  Serial.println("\nStarting Sistem Bel 24 Jam - Tampilan Sederhana...");

 

  systemStartTime = millis();

  pinMode(ledPin, OUTPUT);

  digitalWrite(ledPin, LOW);

 

  // Initialize tombol

  initButtons();

 

  // Initialize LCD

  lcd.init();

  lcd.backlight();

  lcd.setCursor(0, 0);

  lcd.print("SISTEM BEL 24J");

  lcd.setCursor(0, 1);

  lcd.print("Tampilan Sederhana");

  delay(2000);

 

  // Log system start

  logSystemEvent("System starting - Tampilan Sederhana");

 

  // Initialize components

  rtcFound = initRTC();

  dfPlayerReady = initDFPlayer();

 

  spiffsMounted = SPIFFS.begin(true);

  if(spiffsMounted) {

    Serial.println("SPIFFS mounted");

    logSystemEvent("SPIFFS mounted successfully");

    loadSchedule();

   

    // Load system config jika ada

    if (SPIFFS.exists("/system_config.json")) {

      restoreSystemConfig();

    }

  } else {

    Serial.println("Gagal mount SPIFFS!");

    logSystemEvent("Failed to mount SPIFFS");

  }

 

  // Connect to WiFi

  WiFi.begin(ssid, password);

  Serial.print("Menghubungkan WiFi");

  logSystemEvent("Connecting to WiFi: " + String(ssid));

 

  int attempts = 0;

  while(WiFi.status() != WL_CONNECTED && attempts < 30) {

    Serial.print(".");

    delay(500);

    attempts++;

  }

 

  wifiConnected = (WiFi.status() == WL_CONNECTED);

  if(wifiConnected) {

    Serial.println("\nWiFi terhubung! IP: " + WiFi.localIP().toString());

    logSystemEvent("WiFi connected - IP: " + WiFi.localIP().toString());

    timeClient.begin();

   

    if(rtcFound) {

      readTimeFromRTC();

      Serial.println("Menggunakan waktu dari " + rtcType + ": " + formatTime(currentHour, currentMinute, currentSecond));

      logSystemEvent("Using time from " + rtcType + ": " + formatTime(currentHour, currentMinute, currentSecond));

     

      // Auto-sync pertama kali jika WiFi tersedia

      if (autoSyncEnabled) {

        Serial.println("🔄 Auto-sync pertama kali...");

        logSystemEvent("Performing initial auto-sync");

        syncRTCWithNTP();

      }

    } else {

      Serial.println("Menggunakan NTP sebagai sumber waktu utama");

      logSystemEvent("Using NTP as primary time source");

      // Jika tidak ada RTC, langsung ambil dari NTP

      if (timeClient.forceUpdate()) {

        currentHour = timeClient.getHours();

        currentMinute = timeClient.getMinutes();

        currentSecond = timeClient.getSeconds();

        currentDay = timeClient.getDay();

       

        unsigned long epochTime = timeClient.getEpochTime();

        struct tm *ptm = gmtime((time_t *)&epochTime);

        currentDate = ptm->tm_mday;

        currentMonth = ptm->tm_mon + 1;

        currentYear = ptm->tm_year + 1900;

        Serial.println("Waktu NTP: " + formatTime(currentHour, currentMinute, currentSecond));

        logSystemEvent("NTP time set: " + formatTime(currentHour, currentMinute, currentSecond));

      }

    }

  } else {

    Serial.println("\nGagal terhubung WiFi!");

    logSystemEvent("Failed to connect to WiFi");

    addNotification("Gagal terhubung WiFi", 2);

   

    if(rtcFound) {

      readTimeFromRTC();

      Serial.println("Menggunakan waktu dari " + rtcType + ": " + formatTime(currentHour, currentMinute, currentSecond));

      logSystemEvent("Using time from " + rtcType + ": " + formatTime(currentHour, currentMinute, currentSecond));

    } else {

      currentHour = 12;

      currentMinute = 0;

      currentSecond = 0;

      currentDay = 0;

      currentDate = 1;

      currentMonth = 1;

      currentYear = 2023;

      Serial.println("Menggunakan waktu default: " + formatTime(currentHour, currentMinute, currentSecond));

      logSystemEvent("Using default time: " + formatTime(currentHour, currentMinute, currentSecond));

      addNotification("Menggunakan waktu default", 2);

    }

  }

 

  setupWebServer();

  printSystemStatus();

 

  // Initial health check

  performHealthCheck();

 

  // Tampilan awal sederhana

  lcd.clear();

  lcd.setCursor(0, 0);

  lcd.print("Sistem Siap");

  lcd.setCursor(0, 1);

  lcd.print("Tampilan Sederhana");

  delay(2000);

 

  addNotification("System started - Tampilan Sederhana", 1);

 

  // Kembali ke tampilan utama (jam & tanggal)

  currentMenuState = MAIN_DISPLAY;

}

 

// ===== MODIFIKASI: Main Loop dengan fitur baru =====

void loop() {

  // Auto-sync RTC dengan NTP

  checkAutoSync();

 

  // Update NTP time periodically

  if(wifiConnected && millis() - lastNTPUpdate > NTP_UPDATE_INTERVAL) {

    updateTimeFromNTP();

  }

 

  // Handle tombol

  handleButtons();

 

  // Update blink untuk editing mode

  if (editing) {

    updateBlink();

  }

 

  // Check power saving

  checkPowerSaving();

 

  // Health check periodic

  if (millis() - lastHealthCheck > HEALTH_CHECK_INTERVAL) {

    performHealthCheck();

    lastHealthCheck = millis();

  }

 

  // Check system resources

  if (millis() - lastMemoryCheck > 30000) {

    checkSystemResources();

    lastMemoryCheck = millis();

  }

 

  // Update display every second

  static unsigned long lastLCDUpdate = 0;

  if(millis() - lastLCDUpdate >= 1000) {

    lastLCDUpdate = millis();

   

    if(wifiConnected && !rtcFound) {

      // Gunakan NTP jika tidak ada RTC

      timeClient.update();

      currentHour = timeClient.getHours();

      currentMinute = timeClient.getMinutes();

      currentSecond = timeClient.getSeconds();

      currentDay = timeClient.getDay();

     

      unsigned long epochTime = timeClient.getEpochTime();

      struct tm *ptm = gmtime((time_t *)&epochTime);

      currentDate = ptm->tm_mday;

      currentMonth = ptm->tm_mon + 1;

      currentYear = ptm->tm_year + 1900;

    } else if (rtcFound) {

      // Gunakan RTC jika tersedia

      readTimeFromRTC();

    } else {

      // Increment waktu manual jika tidak ada RTC dan WiFi

      currentSecond++;

      if(currentSecond >= 60) {

        currentSecond = 0;

        currentMinute++;

        if(currentMinute >= 60) {

          currentMinute = 0;

          currentHour++;

          if(currentHour >= 24) {

            currentHour = 0;

            currentDay = (currentDay + 1) % 7;

            currentDate++;

            if(currentDate > 31) {

              currentDate = 1;

              currentMonth++;

              if(currentMonth > 12) {

                currentMonth = 1;

                currentYear++;

              }

            }

          }

        }

      }

    }

   

    updateLCDDisplay();

  }

 

  // Check and play bells

  static unsigned long lastBellCheck = 0;

  if(millis() - lastBellCheck >= BELL_CHECK_INTERVAL) {

    lastBellCheck = millis();

    if (!emergencyMode) {

      handleBellPlaying();

    }

  }

 

  // Print status periodically

  static unsigned long lastStatusPrint = 0;

  if(millis() - lastStatusPrint > 300000) { // 5 menit

    lastStatusPrint = millis();

    printSystemStatus();

  }

 

  // Clean old notifications (older than 24 hours)

  static unsigned long lastNotificationCleanup = 0;

  if (millis() - lastNotificationCleanup > 24 * 3600 * 1000) {

    unsigned long currentTime = millis();

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

      if (currentTime - notifications[i].timestamp > 24 * 3600 * 1000) {

        for (int j = i; j < notificationCount - 1; j++) {

          notifications[j] = notifications[j + 1];

        }

        notificationCount--;

        i--;

      }

    }

    lastNotificationCleanup = currentTime;

  }

}