#include
<WiFi.h>
#include
<AsyncTCP.h>
#include
<ESPAsyncWebServer.h>
#include
<LiquidCrystal_I2C.h>
#include
<DFRobotDFPlayerMini.h>
#include
<ArduinoJson.h>
#include
<Preferences.h>
// --- LIBRARY BARU: Preferences API ---
#include
"time.h"
//
=======================================================
// --- KONFIGURASI UMUM (HARAP
DIUBAH) ---
//
=======================================================
// --- PIN & PERANGKAT ---
#define
I2C_SDA_PIN 21
#define
I2C_SCL_PIN 22
#define
DFPLAYER_BUSY_PIN
4
#define
BUTTON_UP_PIN 12
#define
BUTTON_DOWN_PIN 13
#define
BUTTON_OK_PIN 14
#define
BUTTON_BACK_PIN 15
// --- KONFIGURASI NVS FLASH
(PENGGANTI EEPROM) ---
const
char* NVS_NAMESPACE = "bell_config";
// Namespace untuk grup setting
const
char* NVS_KEY_JADWAL = "jadwal_data";
// Key untuk data jadwal
// --- OBJEK INSIALISASI ---
LiquidCrystal_I2C lcd(0x27,
16, 2);
DFRobotDFPlayerMini myDFPlayer;
HardwareSerial mySoftwareSerial(2);
// Serial2 untuk DFPlayer (RX=17, TX=16)
AsyncWebServer server(80);
Preferences preferences;
// Objek global Preferences
// --- KONFIGURASI JARINGAN &
OTENTIKASI (GANTI INI!) ---
const
char* ssid = "Tes123";
const
char* password = "1234Dcba12";
// *** KONFIGURASI USERNAME DAN
PASSWORD WEB ***
const
char* HTTP_USERNAME = "admin";
const
char* HTTP_PASSWORD = "123";
// --- KONFIGURASI WAKTU NTP ---
const
char* ntpServer = "pool.ntp.org";
const
long
gmtOffset_sec = 7 * 3600;
// GMT+7 untuk WIB
const
int daylightOffset_sec = 0;
// --- STRUKTUR DATA JADWAL ---
struct
JadwalBell {
int
dayOfWeek; // 0=Minggu, 1=Senin, ..., 6=Sabtu
int
jam;
int
menit;
int
track;
};
#define
MAX_SCHEDULES 50
JadwalBell jadwal[MAX_SCHEDULES];
int
currentScheduleCount = 0;
// --- VARIABEL GLOBAL & PROTOTIPE
(Dihapus) ---
unsigned
long lastDisplayUpdate
= 0;
unsigned
long lastBellCheck = 0;
bool
bellTelahBerbunyiHariIni[MAX_SCHEDULES]
= {false};
int
currentDayOfWeek = 0;
// Deklarasi fungsi-fungsi
void
handleRoot(AsyncWebServerRequest
*request);
void
handleJadwalAPI(AsyncWebServerRequest
*request);
void
handleTimeSync(AsyncWebServerRequest
*request);
void
notFound(AsyncWebServerRequest
*request);
void
setupRoutes();
void
initWiFi();
void
initTime();
void
initializeDefaultJadwal();
void
loadJadwal();
void
saveJadwal();
void
checkBell();
void
updateDisplay();
void
handleButtons();
bool
checkAuth(AsyncWebServerRequest
*request);
//
=======================================================
// SETUP DAN LOOP UTAMA
// =======================================================
void
setup()
{
Serial.begin(115200);
Serial.println("---
DEBUG SYSTEM START (USING PREFERENCES) ---");
// --- 1. Inisialisasi
Perangkat Keras ---
lcd.begin();
lcd.backlight();
lcd.print("Bell
Otomatis");
lcd.setCursor(0,
1);
lcd.print("Memulai...");
pinMode(DFPLAYER_BUSY_PIN,
INPUT);
pinMode(BUTTON_UP_PIN,
INPUT_PULLUP);
pinMode(BUTTON_DOWN_PIN,
INPUT_PULLUP);
pinMode(BUTTON_OK_PIN,
INPUT_PULLUP);
pinMode(BUTTON_BACK_PIN,
INPUT_PULLUP);
// --- 2. Inisialisasi
Preferences & Jadwal ---
if
(!preferences.begin(NVS_NAMESPACE,
false))
{ // Memulai sesi R/W (false)
Serial.println("FATAL
ERROR: Gagal memulai Preferences!");
}
else {
Serial.printf("DEBUG:
Preferences dimulai di namespace: %s\n", NVS_NAMESPACE);
}
loadJadwal();
// --- 3. Inisialisasi
DFPlayer Mini ---
mySoftwareSerial.begin(9600,
SERIAL_8N1, 17, 16);
if
(!myDFPlayer.begin(mySoftwareSerial))
{
Serial.println("ERROR:
DFPlayer Gagal! Periksa kabel TX/RX.");
}
else {
Serial.println("DEBUG:
DFPlayer OK.");
myDFPlayer.volume(20);
}
// --- 4. Inisialisasi WiFi
& Waktu ---
initWiFi();
initTime();
// --- 5. Inisialisasi Web
Server ---
setupRoutes();
Serial.println("DEBUG:
System Ready. Access IP in browser.");
lcd.clear();
}
void
loop()
{
if
(millis()
- lastDisplayUpdate > 1000)
{
updateDisplay();
lastDisplayUpdate = millis();
}
if
(millis()
- lastBellCheck > 1000)
{
checkBell();
lastBellCheck = millis();
}
handleButtons();
}
//
=======================================================
// FUNGSI JARINGAN, WAKTU &
TAMPILAN (Tidak Berubah)
//
=======================================================
void
initWiFi()
{
// ... (Fungsi initWiFi tetap
sama) ...
lcd.setCursor(0,
1);
lcd.print("Koneksi...");
WiFi.begin(ssid,
password);
int
attempt = 0;
while
(WiFi.status()
!= WL_CONNECTED && attempt < 20)
{
delay(500);
Serial.print(".");
attempt++;
}
if(WiFi.status()
== WL_CONNECTED){
Serial.print("\nDEBUG:
WiFi Terkoneksi. IP: ");
Serial.println(WiFi.localIP());
lcd.clear();
lcd.print("IP:");
lcd.print(WiFi.localIP());
delay(1000);
}
else {
Serial.println("\nERROR:
Gagal koneksi WiFi.");
lcd.setCursor(0,
1);
lcd.print("WIFI
GAGAL!");
delay(3000);
}
}
void
initTime()
{
configTime(gmtOffset_sec,
daylightOffset_sec, ntpServer);
Serial.println("DEBUG:
Waktu NTP dikonfigurasi ulang.");
}
void
updateDisplay()
{
struct
tm timeinfo;
if
(!getLocalTime(&timeinfo))
{
lcd.setCursor(0,
0);
lcd.print("WAKTU
GAGAL SYNC");
lcd.setCursor(0,
1);
lcd.print("
");
return;
}
char
timeStr[9];
strftime(timeStr,
9, "%H:%M:%S",
&timeinfo);
lcd.setCursor(0,
0);
lcd.print(timeStr);
currentDayOfWeek = timeinfo.tm_wday;
const
char* hari[] = {"MIN",
"SEN", "SEL",
"RAB", "KAM",
"JUM", "SAB"};
lcd.setCursor(10,
0);
lcd.print(hari[currentDayOfWeek]);
lcd.print("
");
char
dateStr[11];
strftime(dateStr,
11, "%d/%m/%y",
&timeinfo);
lcd.setCursor(0,
1);
lcd.print("Tgl:
");
lcd.print(dateStr);
lcd.print("
");
}
void
checkBell()
{
struct
tm timeinfo;
if
(!getLocalTime(&timeinfo))
return;
int
currentHour = timeinfo.tm_hour;
int
currentMinute = timeinfo.tm_min;
int
currentSecond = timeinfo.tm_sec;
int
currentDay = timeinfo.tm_wday;
if
(currentHour == 0
&& currentMinute == 0 &&
currentSecond == 0)
{
memset(bellTelahBerbunyiHariIni,
false, sizeof(bellTelahBerbunyiHariIni));
}
for
(int
i = 0; i < currentScheduleCount; i++)
{
if
(jadwal[i].dayOfWeek
== currentDay &&
currentHour == jadwal[i].jam
&&
currentMinute == jadwal[i].menit
&&
currentSecond == 0 &&
!bellTelahBerbunyiHariIni[i])
{
Serial.printf("BELL
AKTIF: Hari %d, Jam %02d:%02d, Track %d\n",
currentDay, currentHour, currentMinute, jadwal[i].track);
lcd.clear();
lcd.setCursor(0,
0);
lcd.print("<<<
BELL BERBUNYI! >>>");
lcd.setCursor(0,
1);
lcd.printf("Track:
%d", jadwal[i].track);
myDFPlayer.play(jadwal[i].track);
bellTelahBerbunyiHariIni[i]
= true;
lastDisplayUpdate = millis()
+ 3000;
}
}
}
void
handleButtons()
{
// Tombol diabaikan
dalam mode WebServer
}
//
=======================================================
// FUNGSI PREFERENCES API (PENGGANTI
EEPROM)
// =======================================================
void
initializeDefaultJadwal()
{
currentScheduleCount = 0;
if
(currentScheduleCount <
MAX_SCHEDULES) {
jadwal[currentScheduleCount++]
= {1, 7,
0, 1};
}
if
(currentScheduleCount <
MAX_SCHEDULES) {
jadwal[currentScheduleCount++]
= {5, 15,
30, 2};
}
Serial.println("DEBUG:
Jadwal default diinisialisasi (2 baris).");
}
void
loadJadwal()
{
Serial.println("DEBUG:
Memulai load Jadwal dari NVS...");
// Mengambil string
JSON dari key NVS_KEY_JADWAL. Default adalah string kosong ("")
String jsonString = preferences.getString(NVS_KEY_JADWAL,
"");
int
len = jsonString.length();
Serial.printf("DEBUG:
Panjang data yang dibaca dari NVS: %d bytes\n",
len);
if
(len == 0)
{
Serial.println("WARN:
NVS Kosong atau data lama tidak ditemukan. Memuat default.");
initializeDefaultJadwal();
return;
}
Serial.print("DEBUG:
JSON String dari NVS: ");
Serial.println(jsonString);
StaticJsonDocument<4096>
doc;
DeserializationError
error = deserializeJson(doc,
jsonString);
if
(error)
{
Serial.print("ERROR:
Gagal deserialize JSON dari NVS (");
Serial.print(error.c_str());
Serial.println(").
Data rusak. Memuat default.");
preferences.remove(NVS_KEY_JADWAL);
// Hapus data yang rusak
initializeDefaultJadwal();
return;
}
JsonArray array = doc.as<JsonArray>();
currentScheduleCount = 0;
for
(JsonObject obj : array)
{
if
(currentScheduleCount <
MAX_SCHEDULES) {
jadwal[currentScheduleCount].dayOfWeek
= obj["day"].as<int>();
jadwal[currentScheduleCount].jam
= obj["jam"].as<int>();
jadwal[currentScheduleCount].menit
= obj["menit"].as<int>();
jadwal[currentScheduleCount].track
= obj["track"].as<int>();
currentScheduleCount++;
}
}
Serial.printf("DEBUG:
Jadwal berhasil dimuat dari NVS. Total: %d baris.\n",
currentScheduleCount);
}
void
saveJadwal()
{
Serial.println("DEBUG:
Memulai save Jadwal ke NVS...");
StaticJsonDocument<4096>
doc; // Kapasitas 4KB
JsonArray array = doc.to<JsonArray>();
for
(int
i = 0; i < currentScheduleCount; i++)
{
JsonObject obj = array.add<JsonObject>();
obj["day"]
= jadwal[i].dayOfWeek;
obj["jam"]
= jadwal[i].jam;
obj["menit"]
= jadwal[i].menit;
obj["track"]
= jadwal[i].track;
Serial.printf("DEBUG:
Menyimpan item %d: D:%d, H:%d, M:%d, T:%d\n",
i+1,
jadwal[i].dayOfWeek,
jadwal[i].jam,
jadwal[i].menit,
jadwal[i].track);
}
String output;
serializeJson(doc,
output);
Serial.printf("DEBUG:
Panjang JSON yang akan disimpan: %d bytes\n",
output.length());
//
preferences.putString() menulis data ke NVS Flash dengan aman dan cepat
size_t
writtenBytes = preferences.putString(NVS_KEY_JADWAL,
output);
if
(writtenBytes > 0)
{
Serial.printf("SUCCESS:
Jadwal berhasil disimpan dan dikomit ke NVS! %d bytes tertulis.\n",
writtenBytes);
}
else {
Serial.println("ERROR:
Gagal menyimpan jadwal ke NVS!");
}
}
// =======================================================
// FUNGSI WEB SERVER ASYNC & HTML
(Tidak Berubah)
//
=======================================================
bool
checkAuth(AsyncWebServerRequest
*request)
{
if
(!request->authenticate(HTTP_USERNAME,
HTTP_PASSWORD)) {
request->requestAuthentication();
return
false;
}
return
true;
}
void
setupRoutes()
{
server.on("/",
HTTP_GET, handleRoot);
server.on("/api/jadwal",
HTTP_GET, handleJadwalAPI);
server.on("/api/jadwal",
HTTP_POST, handleJadwalAPI);
server.on("/api/time_sync",
HTTP_POST, handleTimeSync);
server.onNotFound(notFound);
server.begin();
}
void
notFound(AsyncWebServerRequest
*request)
{
request->send(404,
"text/plain",
"Halaman Tidak Ditemukan");
}
void
handleJadwalAPI(AsyncWebServerRequest
*request)
{
if
(!checkAuth(request))
return;
if
(request->method()
== HTTP_GET) {
StaticJsonDocument<4096>
doc;
JsonArray array = doc.to<JsonArray>();
for
(int
i = 0; i < currentScheduleCount; i++)
{
JsonObject obj = array.add<JsonObject>();
obj["day"]
= jadwal[i].dayOfWeek;
obj["jam"]
= jadwal[i].jam;
obj["menit"]
= jadwal[i].menit;
obj["track"]
= jadwal[i].track;
}
String output;
serializeJson(doc,
output);
request->send(200,
"application/json",
output);
}
else if
(request->method()
== HTTP_POST) {
Serial.println("---
DEBUG: Menerima POST request /api/jadwal ---");
if
(request->hasParam("plain",
true))
{
String
jsonPayload = request->getParam("plain",
true)->value();
Serial.print("DEBUG:
Payload diterima: ");
Serial.println(jsonPayload);
StaticJsonDocument<4096>
doc;
DeserializationError error = deserializeJson(doc,
jsonPayload);
if
(error)
{
Serial.print("ERROR:
Gagal deserialize JSON dari payload: ");
Serial.println(error.c_str());
request->send(400,
"text/plain",
"Gagal memproses JSON: "
+ String(error.c_str()));
return;
}
JsonArray array
= doc.as<JsonArray>();
currentScheduleCount = 0;
for
(JsonObject obj : array)
{
if
(currentScheduleCount <
MAX_SCHEDULES) {
jadwal[currentScheduleCount].dayOfWeek
= obj["day"].as<int>();
jadwal[currentScheduleCount].jam
= obj["jam"].as<int>();
jadwal[currentScheduleCount].menit
= obj["menit"].as<int>();
jadwal[currentScheduleCount].track
= obj["track"].as<int>();
currentScheduleCount++;
}
}
Serial.printf("DEBUG:
JSON berhasil di-parse. Total: %d jadwal.\n",
currentScheduleCount);
saveJadwal();
loadJadwal();
request->send(200,
"text/plain",
"Jadwal berhasil disimpan permanen!");
}
else {
Serial.println("ERROR:
Data POST tidak memiliki parameter 'plain'.");
request->send(400,
"text/plain",
"Data POST tidak valid.");
}
}
}
void
handleTimeSync(AsyncWebServerRequest
*request)
{
if
(!checkAuth(request))
return;
initTime();
delay(100);
struct
tm timeinfo;
if
(getLocalTime(&timeinfo,
5000))
{
char
timeStr[20];
strftime(timeStr,
sizeof(timeStr),
"%H:%M:%S %d/%m/%Y",
&timeinfo);
String response = "Waktu
berhasil disinkronkan: ";
response += timeStr;
request->send(200,
"text/plain",
response);
}
else {
request->send(500,
"text/plain",
"Gagal mendapatkan waktu dari NTP setelah
sinkronisasi ulang.");
}
}
// =alah satu fungsi web (handleRoot)
tetap sama seperti sebelumnya
void
handleRoot(AsyncWebServerRequest
*request)
{
if
(!request->authenticate(HTTP_USERNAME,
HTTP_PASSWORD)) {
request->requestAuthentication();
return;
}
String html = R"rawliteral(
<!DOCTYPE html>
<html lang="id">
<head>
<meta
charset="UTF-8">
<meta
name="viewport" content="width=device-width,
initial-scale=1.0">
<title>Kontrol
Bell Otomatis ESP32</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background-color:
#f4f7f6; color: #333; margin: 0; padding: 20px; }
.container { max-width: 900px; margin: auto; background: #fff; padding: 30px;
border-radius: 12px; box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1); }
h1, h2 {
color: #007bff; border-bottom: 2px solid #e0e0e0; padding-bottom: 10px;
margin-top: 20px; }
.status-box { background: #e9ecef; padding: 15px; border-radius: 8px;
margin-bottom: 20px; display: flex; justify-content: space-between; align-items:
center; }
.status-box p { margin: 0; font-weight: 600; }
#currentTime { font-size: 1.5em; color: #28a745; }
.day-section { border: 1px solid #ddd; border-radius: 8px; margin-bottom: 20px;
padding: 15px; background: #ffffff; }
.day-section h3 { background-color: #f0f0f0; padding: 10px; margin: -15px -15px
15px -15px; border-radius: 8px 8px 0 0; display: flex; justify-content:
space-between; align-items: center; }
.day-section table { width: 100%; border-collapse: collapse; }
.day-section th, .day-section td { padding: 8px; text-align: left;
border-bottom: 1px solid #eee; }
.day-section th { background-color: #007bff; color: white; }
.btn {
padding: 8px 12px; border: none; border-radius: 5px; cursor: pointer;
font-weight: bold; transition: background-color 0.3s; margin-left: 5px;}
.btn-primary { background-color: #007bff; color: white; }
.btn-success { background-color: #28a745; color: white; }
.btn-danger
{ background-color: #dc3545; color: white; }
.input-time { width: 65px; padding: 5px; border: 1px solid #ccc; border-radius:
4px; box-sizing: border-box; }
#notification { padding: 10px; margin-bottom: 15px; border-radius: 5px; display:
none; }
.success
{ background-color: #d4edda; color: #155724; border: 1px solid #c3e6cb; }
.error {
background-color: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; }
</style>
</head>
<body>
<div
class="container">
<h1>Kontrol
Bell Otomatis ESP32</h1>
<div
id="notification"></div>
<h2>Status Perangkat</h2>
<div
class="status-box">
<p>IP Address: <span id="ipAddress">Sedang
Memuat...</span></p>
<p>Waktu Browser: <span
id="currentTime">--:--:--</span></p>
</div>
<h2>Pengaturan Waktu & Sinkronisasi</h2>
<div>
<button class="btn btn-success"
onclick="syncTime()">Kalibrasi Waktu (NTP)</button>
<p style="margin-top:10px; color:#555;">Klik untuk
memaksa sinkronisasi ulang waktu ESP32 dengan NTP server.</p>
</div>
<h2>Jadwal Bell Mingguan</h2>
<div
id="scheduleContainer">
</div>
<button class="btn btn-success" style="margin-top: 20px;
float: right;" onclick="saveSchedule()">SIMPAN JADWAL
PERMANEN</button>
</div>
<script>
const
SCHEDULE_API = '/api/jadwal';
const
TIME_API = '/api/time_sync';
let
schedules = [];
const
DAY_NAMES = ["Minggu", "Senin", "Selasa",
"Rabu", "Kamis", "Jumat", "Sabtu"];
const
DAY_VALUES = [0, 1, 2, 3, 4, 5, 6];
let
lastId = 0;
function
showNotification(message, type) {
const notif = document.getElementById('notification');
notif.textContent = message;
notif.className = '';
notif.classList.add(type);
notif.style.display = 'block';
setTimeout(() => { notif.style.display = 'none'; }, 5000);
}
function
renderSchedule() {
const container = document.getElementById('scheduleContainer');
container.innerHTML = '';
DAY_VALUES.forEach(dayIndex => {
const daySchedules = schedules.filter(sch => sch.dayOfWeek
=== dayIndex).sort((a, b) => {
if (a.jam !== b.jam) return a.jam - b.jam;
return a.menit - b.menit;
});
const section = document.createElement('div');
section.className = 'day-section';
section.innerHTML = `
<h3>
${DAY_NAMES[dayIndex]}
<button class="btn
btn-primary" onclick="addNewRow(${dayIndex})">+
Tambah</button>
</h3>
<table>
<thead>
<tr>
<th>#</th>
<th>Jam (0-23)</th>
<th>Menit (0-59)</th>
<th>Track MP3 (1-255)</th>
<th>Aksi</th>
</tr>
</thead>
<tbody
id="scheduleBody-${dayIndex}">
</tbody>
</table>
`;
container.appendChild(section);
const body =
document.getElementById(`scheduleBody-${dayIndex}`);
daySchedules.forEach((sch, index) => {
const row = body.insertRow();
row.innerHTML = `
<td>${index +
1}</td>
<td><input
type="number" min="0" max="23"
value="${sch.jam}" class="input-time" onchange="updateScheduleInMemory(this,
'jam', ${sch.id})"></td>
<td><input
type="number" min="0" max="59"
value="${sch.menit}" class="input-time"
onchange="updateScheduleInMemory(this, 'menit',
${sch.id})"></td>
<td><input
type="number" min="1" max="255"
value="${sch.track}" class="input-time"
onchange="updateScheduleInMemory(this, 'track',
${sch.id})"></td>
<td>
<button
class="btn btn-danger" onclick="deleteRow(${sch.id})">Hapus</button>
</td>
`;
});
});
}
function
processSchedules(rawSchedules) {
return rawSchedules.map(sch => {
sch.id = ++lastId;
return sch;
});
}
function
updateScheduleInMemory(inputElement, key, schId) {
let val = inputElement.value;
const scheduleItem = schedules.find(sch => sch.id === schId);
if (scheduleItem) {
let intVal = parseInt(val);
if (val === "") {
if (key === 'jam') scheduleItem.jam = NaN;
else if (key === 'menit') scheduleItem.menit
= NaN;
else if (key === 'track') scheduleItem.track
= NaN;
return;
}
if (isNaN(intVal)) {
showNotification("Input harus
berupa angka valid.", "error");
return;
}
if (key === 'jam') scheduleItem.jam = intVal;
else if (key === 'menit') scheduleItem.menit = intVal;
else if (key === 'track') scheduleItem.track = intVal;
} else {
showNotification("Error: Jadwal tidak ditemukan
di memori.", "error");
}
}
function
addNewRow(dayIndex) {
if (schedules.length >= 50) {
showNotification("Batas maksimum jadwal (50)
telah tercapai.", "error");
return;
}
schedules.push({ dayOfWeek: dayIndex, jam: 7, menit: 0, track: 1, id:
++lastId });
renderSchedule();
}
function
deleteRow(schId) {
if (confirm("Yakin ingin menghapus jadwal ini?")) {
schedules = schedules.filter(sch => sch.id !== schId);
renderSchedule();
}
}
async
function fetchSchedule() {
try {
const response = await fetch(SCHEDULE_API);
if (!response.ok) throw new Error("Gagal memuat
jadwal dari server.");
const rawSchedules = await response.json();
schedules = processSchedules(rawSchedules);
renderSchedule();
} catch (error) {
showNotification("Gagal memuat jadwal dari NVS:
" + error.message, "error");
}
}
async
function saveSchedule() {
const finalSchedules = schedules.map(sch => ({
day: sch.dayOfWeek,
jam: sch.jam,
menit: sch.menit,
track: sch.track
}));
let validationError = false;
finalSchedules.forEach((sch, index) => {
let displayDay = DAY_NAMES[sch.day] ?? "Tidak
Diketahui";
let displayTime = isNaN(sch.jam) || isNaN(sch.menit) ?
'??:??' : `${sch.jam}:${sch.menit}`;
if (
!(sch.day >= 0 && sch.day <=
6) ||
!(sch.jam >= 0 && sch.jam <=
23) ||
!(sch.menit >= 0 && sch.menit
<= 59) ||
!(sch.track >= 1 && sch.track
<= 255) ||
isNaN(sch.jam) || isNaN(sch.menit) ||
isNaN(sch.track)
) {
showNotification(`Jadwal ke-${index + 1}
(${displayDay} ${displayTime}) tidak valid. Periksa nilai Jam (0-23), Menit
(0-59), atau Track (1-255)!`, "error");
validationError = true;
}
});
if (validationError) {
console.log("DEBUG JS: Validasi gagal. Penyimpanan
dibatalkan.");
return;
}
try {
const btn =
document.querySelector('.btn-success[onclick="saveSchedule()"]');
btn.disabled = true;
btn.textContent = "Menyimpan...";
console.log("DEBUG JS: Mengirim data ke
server...");
const response = await fetch(SCHEDULE_API, {
method: 'POST',
headers: { 'Content-Type':
'application/json' },
body: JSON.stringify(finalSchedules)
});
if (!response.ok) {
const errorText = await response.text();
console.error("DEBUG JS: Server
merespon dengan error:", response.status, errorText);
throw new Error("Server gagal menyimpan
data. Respon: " + errorText);
}
showNotification("Jadwal Berhasil Disimpan PERMANEN
di NVS!", "success");
} catch (error) {
showNotification("Gagal menyimpan jadwal: " +
error.message, "error");
} finally {
const btn =
document.querySelector('.btn-success[onclick="saveSchedule()"]');
btn.disabled = false;
btn.textContent = "SIMPAN JADWAL PERMANEN";
fetchSchedule();
}
}
async
function syncTime() {
if (!confirm("Yakin ingin melakukan kalibrasi waktu dengan NTP?"))
return;
try {
const btn =
document.querySelector('.btn-success[onclick="syncTime()"]');
btn.disabled = true;
btn.textContent = "Sinkronisasi...";
const response = await fetch(TIME_API, { method: 'POST'
});
if (!response.ok) throw new Error("Gagal sinkronisasi
waktu.");
const result = await response.text();
showNotification("Waktu berhasil disinkronkan. "
+ result, "success");
} catch (error) {
showNotification("Gagal sinkronisasi waktu: " +
error.message, "error");
} finally {
const btn =
document.querySelector('.btn-success[onclick="syncTime()"]');
btn.disabled = false;
btn.textContent = "Kalibrasi Waktu (NTP)";
}
}
function
updateCurrentTime() {
const now = new Date();
const timeStr = now.toLocaleTimeString('id-ID', { hour: '2-digit',
minute: '2-digit', second: '2-digit' });
document.getElementById('currentTime').textContent = timeStr;
}
document.addEventListener('DOMContentLoaded', () => {
document.getElementById('ipAddress').textContent = window.location.host;
fetchSchedule();
setInterval(updateCurrentTime, 1000);
});
</script>
</body>
</html>
)rawliteral";
request->send(200,
"text/html",
html);
}
0 comments:
Post a Comment