#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;
}
}