// ============================================
// KODE ESP32 - Si-ShoPint (PROFESSIONAL UI & AUDIO) - DENGAN PWM BUZZER
// ============================================
// UPDATE: Tampilan OLED profesional
// UPDATE: Buzzer pasif dengan PWM menggunakan LEDC ESP32
// UPDATE: Variasi nada yang berbeda untuk setiap notifikasi
// ============================================
#include <WiFi.h>
#include <HTTPClient.h>
#include <SPI.h>
#include <MFRC522.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <NTPClient.h>
#include <WiFiUdp.h>
#include <ArduinoJson.h>
// ============================================
// OLED CONFIG
// ============================================
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_ADDR 0x3C
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);
// ============================================
// HARDWARE PINS
// ============================================
#define RST_PIN 27
#define SS_PIN 5
#define BUZZER_PIN 4
#define LED_PIN 16
// PWM Configuration untuk buzzer
#define BUZZER_CHANNEL 0 // Channel PWM 0
#define BUZZER_RESOLUTION 8 // 8-bit resolution (0-255)
#define BUZZER_FREQ_BASE 1000 // Base frequency (akan diubah sesuai nada)
// ============================================
// BUZZER FREQUENCIES (Hz) untuk buzzer pasif
// ============================================
#define NOTE_C3 131
#define NOTE_D3 147
#define NOTE_E3 165
#define NOTE_F3 175
#define NOTE_G3 196
#define NOTE_A3 220
#define NOTE_B3 247
#define NOTE_C4 262
#define NOTE_D4 294
#define NOTE_E4 330
#define NOTE_F4 349
#define NOTE_G4 392
#define NOTE_A4 440
#define NOTE_B4 494
#define NOTE_C5 523
#define NOTE_D5 587
#define NOTE_E5 659
#define NOTE_F5 698
#define NOTE_G5 784
#define NOTE_A5 880
#define NOTE_B5 988
#define NOTE_C6 1047
#define NOTE_CS6 1109
#define NOTE_D6 1175
#define NOTE_DS6 1245
#define NOTE_E6 1319
#define NOTE_F6 1397
#define NOTE_FS6 1480
#define NOTE_G6 1568
#define NOTE_GS6 1661
#define NOTE_A6 1760
#define NOTE_AS6 1865
#define NOTE_B6 1976
// ============================================
// RFID
// ============================================
MFRC522 mfrc522(SS_PIN, RST_PIN);
MFRC522::MIFARE_Key key;
// ============================================
// WIFI & TIME
// ============================================
const char* ssid = "Galapagos";
const char* password = "@Galapagos123";
WiFiUDP ntpUDP;
NTPClient timeClient(ntpUDP, "pool.ntp.org", 25200); // GMT+7
// ============================================
// API CONFIG
// ============================================
const char* serverUrl = "https://zidcreative.com/sishopint/api/presensi.php";
const char* kegiatanUrl = "https://zidcreative.com/sishopint/api/kegiatan.php";
const int USER_ID = 4;
const char* LEMBAGA_NAME = "TPQ Al-Munawwarah";
// ============================================
// STRUKTUR DATA KEGIATAN
// ============================================
#define MAX_KEGIATAN 20
struct Kegiatan {
int id;
String nama;
int jamMulai;
int menitMulai;
int jamSelesai;
int menitSelesai;
int poin;
bool aktif;
};
Kegiatan daftarKegiatan[MAX_KEGIATAN];
int jumlahKegiatan = 0;
unsigned long lastKegiatanUpdate = 0;
const unsigned long KEGIATAN_UPDATE_INTERVAL = 3600000; // Update setiap 1 jam
// ============================================
// VARIABEL
// ============================================
String currentTimestamp = "";
int dailySholatCount = 0;
int dailyKegiatanCount = 0;
bool wifiConnected = false;
// ============================================
// FUNGSI PWM BUZZER UNTUK BUZZER PASIF
// ============================================
// Inisialisasi PWM untuk buzzer
void initBuzzerPWM() {
// Setup channel PWM dengan frekuensi default
ledcSetup(BUZZER_CHANNEL, BUZZER_FREQ_BASE, BUZZER_RESOLUTION);
ledcAttachPin(BUZZER_PIN, BUZZER_CHANNEL);
ledcWrite(BUZZER_CHANNEL, 0); // Matikan buzzer
}
// Fungsi untuk membunyikan buzzer dengan frekuensi dan durasi tertentu
void playBuzzerTone(int frequency, int duration) {
if (frequency <= 0) {
ledcWrite(BUZZER_CHANNEL, 0);
return;
}
// Set frekuensi PWM
ledcSetup(BUZZER_CHANNEL, frequency, BUZZER_RESOLUTION);
ledcAttachPin(BUZZER_PIN, BUZZER_CHANNEL);
// Set duty cycle 50% untuk volume maksimal
int dutyCycle = (1 << (BUZZER_RESOLUTION - 1)) - 1; // 127 untuk 8-bit
ledcWrite(BUZZER_CHANNEL, dutyCycle);
// Tunggu selama durasi yang ditentukan
delay(duration);
// Matikan buzzer
ledcWrite(BUZZER_CHANNEL, 0);
}
// Fungsi untuk memainkan melodi dengan beberapa nada
void playMelody(int frequencies[], int durations[], int count) {
for (int i = 0; i < count; i++) {
if (frequencies[i] > 0) {
playBuzzerTone(frequencies[i], durations[i]);
delay(durations[i] * 0.1); // Jeda kecil antar nada
} else {
delay(durations[i]); // Rest/silence
}
}
}
// ============================================
// FUNGSI TONE CUSTOM DENGAN PWM UNTUK BUZZER PASIF
// ============================================
// Nada startup - melodi naik (C4-E4-G4-C5)
void startupTone() {
int freqs[] = {NOTE_C4, NOTE_E4, NOTE_G4, NOTE_C5};
int durs[] = {150, 150, 150, 400};
for (int i = 0; i < 4; i++) {
playBuzzerTone(freqs[i], durs[i]);
delay(20);
}
}
// Nada kartu terdeteksi - double beep cepat (A5-A5)
void cardDetectedTone() {
playBuzzerTone(NOTE_A5, 60);
delay(30);
playBuzzerTone(NOTE_A5, 60);
delay(30);
}
// Nada sukses - melodi ceria (C5-E5-G5-C6)
void successTone() {
int freqs[] = {NOTE_C5, NOTE_E5, NOTE_G5, NOTE_C6};
int durs[] = {100, 100, 100, 300};
for (int i = 0; i < 4; i++) {
playBuzzerTone(freqs[i], durs[i]);
delay(20);
}
}
// Nada error - melodi turun (C5-B4-A4-G4)
void errorTone() {
int freqs[] = {NOTE_C5, NOTE_B4, NOTE_A4, NOTE_G4};
int durs[] = {120, 120, 120, 300};
for (int i = 0; i < 4; i++) {
playBuzzerTone(freqs[i], durs[i]);
delay(20);
}
}
// Nada warning - sirene pendek (A4-F4-A4-F4)
void warningTone() {
int freqs[] = {NOTE_A4, NOTE_F4, NOTE_A4, NOTE_F4};
int durs[] = {150, 150, 150, 300};
for (int i = 0; i < 4; i++) {
playBuzzerTone(freqs[i], durs[i]);
delay(20);
}
}
// Nada kartu invalid - tiga nada pendek menurun (C4-B3-A3-G3)
void invalidCardTone() {
int freqs[] = {NOTE_C4, NOTE_B3, NOTE_A3, NOTE_G3};
int durs[] = {80, 80, 80, 250};
for (int i = 0; i < 4; i++) {
playBuzzerTone(freqs[i], durs[i]);
delay(15);
}
}
// Nada WiFi sukses - dua nada naik (E4-G4-C5)
void wifiSuccessTone() {
playBuzzerTone(NOTE_E4, 100);
delay(20);
playBuzzerTone(NOTE_G4, 100);
delay(20);
playBuzzerTone(NOTE_C5, 200);
delay(20);
}
// Nada WiFi gagal - dua nada sama panjang (C4-C4)
void wifiFailedTone() {
playBuzzerTone(NOTE_C4, 250);
delay(30);
playBuzzerTone(NOTE_C4, 250);
delay(30);
}
// Nada reconnect - satu nada naik pendek (G4-C5)
void reconnectTone() {
playBuzzerTone(NOTE_G4, 100);
delay(20);
playBuzzerTone(NOTE_C5, 200);
delay(20);
}
// Nada data loaded - tiga nada staccato (C5-E5-G5)
void dataLoadedTone() {
playBuzzerTone(NOTE_C5, 60);
delay(10);
playBuzzerTone(NOTE_E5, 60);
delay(10);
playBuzzerTone(NOTE_G5, 60);
delay(10);
playBuzzerTone(NOTE_C6, 200);
delay(10);
}
// Nada duplicate - dua nada sama dengan jeda (F4-F4)
void duplicateTone() {
playBuzzerTone(NOTE_F4, 200);
delay(30);
playBuzzerTone(NOTE_F4, 200);
delay(30);
}
// ============================================
// FUNGSI KONTROL BUZZER PRO (DARI SERVER)
// ============================================
void kontrolBuzzerPro(String buzzerType) {
if (buzzerType == "SHORT_BEEP") {
// Single beep pendek - A5
playBuzzerTone(NOTE_A5, 100);
} else if (buzzerType == "LONG_BEEP") {
// Long beep - G4 panjang
playBuzzerTone(NOTE_G4, 600);
} else if (buzzerType == "DOUBLE_BEEP") {
// Double beep - dua nada berbeda (C5-E5)
playBuzzerTone(NOTE_C5, 100);
delay(20);
playBuzzerTone(NOTE_E5, 100);
} else if (buzzerType == "CONTINUOUS") {
// Continuous beep - alarm naik turun (C5-G5-C5-G5)
for(int i = 0; i < 3; i++) {
playBuzzerTone(NOTE_C5, 200);
delay(20);
playBuzzerTone(NOTE_G5, 200);
delay(20);
}
}
}
// ============================================
// FUNGSI TEST BUZZER UNTUK DEBUG
// ============================================
void testBuzzer() {
Serial.println("Testing buzzer with PWM...");
int testFreqs[] = {262, 330, 392, 523, 659, 784, 880, 1047};
const char* noteNames[] = {"C4", "E4", "G4", "C5", "E5", "G5", "A5", "C6"};
for (int i = 0; i < 8; i++) {
Serial.print("Playing ");
Serial.print(noteNames[i]);
Serial.print(" (");
Serial.print(testFreqs[i]);
Serial.println(" Hz)");
playBuzzerTone(testFreqs[i], 300);
delay(350);
}
Serial.println("Playing startup melody:");
startupTone();
Serial.println("Playing success melody:");
successTone();
Serial.println("Buzzer test complete");
}
// ============================================
// SETUP
// ============================================
void setup() {
Serial.begin(115200);
Serial.println("\n=== Si-ShoPint v2.0 (Dengan PWM Buzzer) ===");
// Init OLED
if (!display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDR)) {
Serial.println("OLED failed");
}
display.clearDisplay();
display.setTextColor(WHITE);
// Init RFID
SPI.begin();
mfrc522.PCD_Init();
for (byte i = 0; i < 6; i++) key.keyByte[i] = 0xFF;
// Init pins
pinMode(BUZZER_PIN, OUTPUT);
pinMode(LED_PIN, OUTPUT);
// Init PWM untuk buzzer
initBuzzerPWM();
// Test buzzer (opsional, bisa di-comment jika tidak perlu)
// testBuzzer();
showSplashScreen();
// Connect WiFi
connectWiFi();
// Init time
timeClient.begin();
timeClient.update();
updateTime();
// Ambil jadwal kegiatan dari server
if (wifiConnected) {
ambilJadwalKegiatan();
} else {
showOfflineMode();
delay(2000);
}
showHomeScreen();
startupTone(); // Startup tone
}
// ============================================
// LOOP
// ============================================
void loop() {
static unsigned long lastTimeUpdate = 0;
static unsigned long lastWifiCheck = 0;
unsigned long now = millis();
// Update time setiap 30 detik
if (now - lastTimeUpdate >= 30000) {
timeClient.update();
updateTime();
lastTimeUpdate = now;
}
// Cek WiFi setiap 10 detik
if (now - lastWifiCheck >= 10000) {
checkWiFi();
lastWifiCheck = now;
}
// Update jadwal kegiatan setiap 1 jam
if (wifiConnected && (now - lastKegiatanUpdate >= KEGIATAN_UPDATE_INTERVAL)) {
ambilJadwalKegiatan();
lastKegiatanUpdate = now;
}
showHomeScreen();
// Check RFID
if (mfrc522.PICC_IsNewCardPresent() && mfrc522.PICC_ReadCardSerial()) {
cardDetectedTone();
processCard();
mfrc522.PICC_HaltA();
mfrc522.PCD_StopCrypto1();
}
delay(100);
}
// ============================================
// FUNGSI WIFI
// ============================================
void connectWiFi() {
showConnectingScreen();
WiFi.begin(ssid, password);
int attempts = 0;
while (WiFi.status() != WL_CONNECTED && attempts < 30) {
delay(500);
Serial.print(".");
attempts++;
if (attempts % 4 == 0) {
showWiFiProgress(attempts);
}
}
if (WiFi.status() == WL_CONNECTED) {
wifiConnected = true;
Serial.println("\nWiFi OK");
Serial.print("IP: ");
Serial.println(WiFi.localIP());
wifiSuccessTone();
showWiFiSuccess();
delay(1000);
} else {
wifiConnected = false;
Serial.println("\nWiFi GAGAL");
wifiFailedTone();
showOfflineMode();
delay(2000);
}
}
void checkWiFi() {
if (WiFi.status() == WL_CONNECTED) {
if (!wifiConnected) {
wifiConnected = true;
Serial.println("WiFi Reconnected");
reconnectTone();
ambilJadwalKegiatan();
}
} else {
if (wifiConnected) {
wifiConnected = false;
Serial.println("WiFi Disconnected");
}
}
}
// ============================================
// FUNGSI AMBIL JADWAL KEGIATAN
// ============================================
void ambilJadwalKegiatan() {
if (WiFi.status() != WL_CONNECTED) {
Serial.println("WiFi offline, tidak bisa ambil jadwal");
return;
}
showLoadingScreen("Muat Jadwal", "Kegiatan...");
Serial.println("Mengambil jadwal kegiatan dari server...");
String token = "2ce9d827ba3116b3152c82aa1782aaf772ea4855949551ef8c37b6fec9eb05bb";
String url = String(kegiatanUrl) +
"?user_id=" + USER_ID +
"&status=aktif" +
"&limit=50" +
"&token=" + token;
HTTPClient http;
http.begin(url);
http.addHeader("Accept", "application/json");
http.setTimeout(10000);
int httpCode = http.GET();
if (httpCode == 200) {
String response = http.getString();
if (parseJadwalKegiatan(response)) {
Serial.print("Berhasil memuat ");
Serial.print(jumlahKegiatan);
Serial.println(" kegiatan");
dataLoadedTone();
showDataLoaded(String(jumlahKegiatan) + " kegiatan");
delay(1000);
} else {
Serial.println("Gagal parse jadwal kegiatan");
errorTone();
showErrorPro("Parse Error", "Gagal parse");
delay(1000);
}
} else {
Serial.print("HTTP Error: ");
Serial.println(httpCode);
errorTone();
showErrorPro("HTTP Error", String(httpCode));
delay(1000);
}
http.end();
lastKegiatanUpdate = millis();
}
// ============================================
// PARSE JADWAL KEGIATAN
// ============================================
bool parseJadwalKegiatan(String json) {
JsonDocument doc;
DeserializationError error = deserializeJson(doc, json);
if (error) {
Serial.print("JSON Error: ");
Serial.println(error.c_str());
return false;
}
jumlahKegiatan = 0;
if (!doc["success"].as<bool>()) {
Serial.println("API return error");
return false;
}
JsonArray kegiatanArray = doc["data"]["kegiatan"].as<JsonArray>();
for (JsonObject keg : kegiatanArray) {
if (jumlahKegiatan >= MAX_KEGIATAN) break;
int id = keg["id"].as<int>();
const char* nama = keg["nama_kegiatan"] | "Unknown";
const char* waktuMulai = keg["waktu_mulai"] | "00:00";
const char* waktuSelesai = keg["waktu_selesai"] | "00:00";
int poin = keg["poin"].as<int>() | 5;
const char* status = keg["status"] | "aktif";
int jamMulai = String(waktuMulai).substring(0, 2).toInt();
int menitMulai = String(waktuMulai).substring(3, 5).toInt();
int jamSelesai = String(waktuSelesai).substring(0, 2).toInt();
int menitSelesai = String(waktuSelesai).substring(3, 5).toInt();
daftarKegiatan[jumlahKegiatan].id = id;
daftarKegiatan[jumlahKegiatan].nama = String(nama);
daftarKegiatan[jumlahKegiatan].jamMulai = jamMulai;
daftarKegiatan[jumlahKegiatan].menitMulai = menitMulai;
daftarKegiatan[jumlahKegiatan].jamSelesai = jamSelesai;
daftarKegiatan[jumlahKegiatan].menitSelesai = menitSelesai;
daftarKegiatan[jumlahKegiatan].poin = poin;
daftarKegiatan[jumlahKegiatan].aktif = (String(status) == "aktif");
jumlahKegiatan++;
}
return (jumlahKegiatan > 0);
}
// ============================================
// FUNGSI CEK KEGIATAN
// ============================================
int cekKegiatanSekarang(int jam, int menit, String &namaKegiatan) {
int waktuSekarang = jam * 60 + menit;
for (int i = 0; i < jumlahKegiatan; i++) {
if (!daftarKegiatan[i].aktif) continue;
int waktuMulai = daftarKegiatan[i].jamMulai * 60 + daftarKegiatan[i].menitMulai;
int waktuSelesai = daftarKegiatan[i].jamSelesai * 60 + daftarKegiatan[i].menitSelesai;
if (waktuSekarang >= waktuMulai && waktuSekarang <= waktuSelesai + 5) {
namaKegiatan = daftarKegiatan[i].nama;
return daftarKegiatan[i].id;
}
}
return 0;
}
// ============================================
// FUNGSI RFID
// ============================================
void processCard() {
Serial.println("\nCard detected!");
showProcessing();
String id_jari = readBlock(5);
String nama = readBlock(4);
if (id_jari.length() == 0) {
Serial.println("Invalid card");
invalidCardTone();
showInvalidCard();
beep(2);
return;
}
if (nama.length() == 0) {
nama = "Peserta " + id_jari.substring(0, 4);
}
Serial.print("ID Jari: ");
Serial.println(id_jari);
Serial.print("Nama: ");
Serial.println(nama);
updateTime();
sendToServer(id_jari, nama);
}
String readBlock(int block) {
byte buffer[18];
byte size = sizeof(buffer);
String result = "";
MFRC522::StatusCode status = mfrc522.PCD_Authenticate(
MFRC522::PICC_CMD_MF_AUTH_KEY_A,
block,
&key,
&(mfrc522.uid)
);
if (status != MFRC522::STATUS_OK) {
return result;
}
status = mfrc522.MIFARE_Read(block, buffer, &size);
if (status != MFRC522::STATUS_OK) {
return result;
}
for (byte i = 0; i < 16; i++) {
if (buffer[i] != 0x00 && buffer[i] != 0xFF) {
result += (char)buffer[i];
}
}
result.trim();
return result;
}
// ============================================
// FUNGSI KIRIM KE SERVER
// ============================================
void sendToServer(String id_jari, String nama) {
if (WiFi.status() != WL_CONNECTED) {
Serial.println("WiFi disconnected");
wifiFailedTone();
showErrorPro("WiFi", "Putus");
beep(2);
return;
}
int jam = currentTimestamp.substring(11, 13).toInt();
int menit = currentTimestamp.substring(14, 16).toInt();
String namaKegiatanTerdeteksi = "";
int kegiatanId = cekKegiatanSekarang(jam, menit, namaKegiatanTerdeteksi);
JsonDocument doc;
doc["id_jari"] = id_jari;
doc["nama"] = nama;
doc["timestamp"] = currentTimestamp;
doc["user_id"] = USER_ID;
if (kegiatanId > 0) {
doc["tipe"] = "kegiatan";
doc["kegiatan_id"] = kegiatanId;
Serial.println("✓ Mengirim sebagai PRESENSI KEGIATAN");
} else {
doc["tipe"] = "sholat";
Serial.println("✓ Mengirim sebagai PRESENSI SHOLAT");
}
String json;
serializeJson(doc, json);
HTTPClient http;
http.begin(serverUrl);
http.addHeader("Content-Type", "application/json");
int httpCode = http.POST(json);
if (httpCode > 0) {
String response = http.getString();
processResponse(response, nama, kegiatanId, namaKegiatanTerdeteksi);
} else {
Serial.println("HTTP Error");
errorTone();
showErrorPro("Server", "Error");
beep(3);
}
http.end();
}
// ============================================
// FUNGSI PROCESS RESPONSE - DIUPDATE
// ============================================
void processResponse(String response, String nama, int kegiatanId, String namaKegiatan) {
JsonDocument doc;
DeserializationError error = deserializeJson(doc, response);
if (error) {
Serial.print("JSON Error: ");
Serial.println(error.c_str());
errorTone();
showErrorPro("Respon", "Error");
beep(3);
return;
}
bool success = doc["success"].as<bool>();
String errorCode = doc["error_code"] | "";
JsonObject feedback = doc["device_feedback"].as<JsonObject>();
String ledColor = feedback["led_color"] | "";
String buzzerType = feedback["buzzer"] | "";
String displayMsg = feedback["display_message"] | "";
kontrolLED(ledColor);
kontrolBuzzerPro(buzzerType);
if (success) {
JsonObject data = doc["data"].as<JsonObject>();
String tipe = data["tipe"] | "sholat";
int poin = data["poin"].as<int>() | 0;
int peserta_id = data["peserta_id"].as<int>() | 0;
// AMBIL TOTAL POIN SANTRI
int total_poin = -1;
if (peserta_id > 0) {
total_poin = getTotalPoinSantri(peserta_id);
}
if (tipe == "kegiatan") {
String namaKegiatanRes = data["nama_kegiatan"] | (namaKegiatan.length() > 0 ? namaKegiatan : "Kegiatan");
String statusPresensi = data["status"] | "hadir";
String waktuMulai = data["waktu_mulai"] | "";
String waktuSelesai = data["waktu_selesai"] | "";
dailyKegiatanCount++;
if (displayMsg.length() > 0) {
// NEW: Panggil fungsi dengan total_poin
showSuccessKegiatanPro(nama, namaKegiatanRes, displayMsg, poin, waktuMulai, waktuSelesai, total_poin);
} else {
String statusText = (statusPresensi == "hadir") ? "Hadir" : "Terlambat";
// NEW: Panggil fungsi dengan total_poin
showSuccessKegiatanPro(nama, namaKegiatanRes, statusText, poin, waktuMulai, waktuSelesai, total_poin);
}
} else {
String sholat = data["sholat"] | "Unknown";
String statusPresensi = data["status"] | "Unknown";
dailySholatCount++;
if (displayMsg.length() > 0) {
// NEW: Panggil fungsi dengan total_poin
showSuccessSholatPro(nama, sholat, displayMsg, poin, total_poin);
} else {
// NEW: Panggil fungsi dengan total_poin
showSuccessSholatPro(nama, sholat, statusPresensi, poin, total_poin);
}
}
} else {
handleErrorPro(errorCode, doc, namaKegiatan);
}
}
// ============================================
// FUNGSI KONTROL HARDWARE
// ============================================
void kontrolLED(String ledColor) {
if (ledColor == "GREEN") {
digitalWrite(LED_PIN, HIGH);
delay(1000);
digitalWrite(LED_PIN, LOW);
} else if (ledColor == "RED") {
for(int i = 0; i < 3; i++) {
digitalWrite(LED_PIN, HIGH);
delay(200);
digitalWrite(LED_PIN, LOW);
delay(200);
}
} else if (ledColor == "YELLOW") {
for(int i = 0; i < 2; i++) {
digitalWrite(LED_PIN, HIGH);
delay(300);
digitalWrite(LED_PIN, LOW);
delay(300);
}
}
}
// ============================================
// FUNGSI HANDLE ERROR PRO
// ============================================
void handleErrorPro(String errorCode, JsonDocument &doc, String namaKegiatan) {
JsonObject data = doc["data"].as<JsonObject>();
Serial.print("Error: ");
Serial.println(errorCode);
if (errorCode == "DUPLICATE_ABSENSI") {
duplicateTone();
String tipe = data["tipe"] | "sholat";
if (tipe == "kegiatan") {
String namaKegiatanErr = data["nama_kegiatan"] | (namaKegiatan.length() > 0 ? namaKegiatan : "Kegiatan");
String waktu = data["waktu_sebelumnya"] | "";
showDuplicateKegiatanPro(namaKegiatanErr, waktu);
} else {
String sholat = data["sholat"] | "Sholat";
String waktu = data["waktu_sebelumnya"] | "";
showDuplicateSholatPro(sholat, waktu);
}
} else if (errorCode == "ABSENSI_DITOLAK") {
warningTone();
String keterangan = data["keterangan"] | "Diluar waktu";
String sholat = data["sholat"] | "";
String namaKegiatanErr = data["nama_kegiatan"] | "";
String displayMsg = doc["device_feedback"]["display_message"] | "";
showDitolakPro(sholat, namaKegiatanErr, keterangan, displayMsg);
} else if (errorCode == "KEGIATAN_NOT_FOUND") {
errorTone();
showErrorPro("Kegiatan", "Tidak ditemukan");
} else if (errorCode == "KEGIATAN_INACTIVE") {
warningTone();
showErrorPro("Kegiatan", "Non-aktif");
} else {
errorTone();
String msg = doc["message"] | "Error";
showErrorPro("Error", msg.substring(0, 12));
}
}
// ============================================
// FUNGSI DISPLAY
// ============================================
void showSplashScreen() {
display.clearDisplay();
display.drawRect(0, 0, 128, 64, WHITE);
display.setTextSize(1);
display.setCursor(25, 10);
display.print("Si-ShoPint");
display.setTextSize(2);
display.setCursor(20, 25);
display.print("v2.0");
display.setTextSize(1);
display.setCursor(15, 48);
display.print(LEMBAGA_NAME);
display.display();
delay(2000);
}
void showHomeScreen() {
display.clearDisplay();
// Nama Lembaga
display.setTextSize(1);
display.setTextColor(WHITE);
int lembagaWidth = strlen(LEMBAGA_NAME) * 6;
int lembagaX = (128 - lembagaWidth) / 2;
display.setCursor(lembagaX, 2);
display.print(LEMBAGA_NAME);
display.drawLine(0, 12, 128, 12, WHITE);
// Jam Digital
display.setTextSize(3);
String timeStr = currentTimestamp.substring(11, 16);
int timeWidth = timeStr.length() * 18;
int timeX = (128 - timeWidth) / 2;
display.setCursor(timeX, 18);
display.print(timeStr);
// Tanggal
display.setTextSize(1);
int year = currentTimestamp.substring(0, 4).toInt();
int month = currentTimestamp.substring(5, 7).toInt();
int day = currentTimestamp.substring(8, 10).toInt();
const char* bulanIndo[] = {"Januari", "Februari", "Maret", "April", "Mei", "Juni",
"Juli", "Agustus", "September", "Oktober", "November", "Desember"};
String tanggalFormat = String(day) + " " + String(bulanIndo[month-1]) + " " + String(year);
int tanggalWidth = tanggalFormat.length() * 6;
int tanggalX = (128 - tanggalWidth) / 2;
display.setCursor(tanggalX, 40);
display.print(tanggalFormat);
// Info Sholat dan Kegiatan
display.drawLine(0, 50, 128, 50, WHITE);
display.setCursor(15, 55);
display.print("S:");
display.print(dailySholatCount);
display.setCursor(60, 55);
display.print("|");
display.setCursor(75, 55);
display.print("K:");
display.print(dailyKegiatanCount);
// WiFi indicator
if (wifiConnected) {
display.fillCircle(118, 6, 2, WHITE);
display.drawLine(115, 9, 121, 9, WHITE);
display.drawLine(112, 12, 124, 12, WHITE);
} else {
display.fillCircle(118, 6, 2, WHITE);
display.drawLine(115, 9, 121, 9, WHITE);
display.drawLine(112, 12, 124, 12, WHITE);
display.drawLine(114, 4, 122, 8, WHITE);
display.drawLine(114, 8, 122, 4, WHITE);
}
display.display();
}
String getDayName() {
unsigned long epoch = timeClient.getEpochTime();
struct tm *ptm = gmtime((time_t *)&epoch);
int dayOfWeek = ptm->tm_wday;
String days[] = {"Minggu", "Senin", "Selasa", "Rabu", "Kamis", "Jumat", "Sabtu"};
return days[dayOfWeek];
}
void showConnectingScreen() {
display.clearDisplay();
display.setTextSize(1);
display.setCursor(20, 15);
display.print("Menghubungkan");
display.setCursor(35, 25);
display.print("ke WiFi");
for(int i = 0; i < 3; i++) {
display.setCursor(50 + (i * 6), 35);
display.print(".");
display.display();
delay(300);
}
display.display();
}
void showWiFiProgress(int attempts) {
display.clearDisplay();
display.setTextSize(1);
display.setCursor(25, 20);
display.print("WiFi Connect");
display.setCursor(30, 35);
int progress = (attempts * 100) / 30;
display.print(progress);
display.print("%");
display.drawRect(20, 45, 88, 8, WHITE);
display.fillRect(22, 47, (progress * 84) / 100, 4, WHITE);
display.display();
}
void showWiFiSuccess() {
display.clearDisplay();
display.fillRect(0, 0, 128, 64, WHITE);
display.setTextColor(BLACK);
display.setTextSize(2);
display.setCursor(25, 15);
display.print("WiFi OK");
display.setTextSize(1);
display.setCursor(15, 40);
display.print(WiFi.localIP().toString());
display.display();
}
void showOfflineMode() {
display.clearDisplay();
display.fillRect(0, 0, 128, 64, WHITE);
display.setTextColor(BLACK);
display.setTextSize(1);
display.setCursor(25, 20);
display.print("MODE OFFLINE");
display.setCursor(20, 35);
display.print("Terbatas");
display.display();
}
void showLoadingScreen(String line1, String line2) {
display.clearDisplay();
display.setTextSize(1);
display.setCursor(25, 15);
display.print(line1);
display.setCursor(25, 25);
display.print(line2);
static int spinner = 0;
const char* spinnerChars = "|/-\\";
display.setCursor(60, 40);
display.print(spinnerChars[spinner]);
spinner = (spinner + 1) % 4;
display.display();
}
void showDataLoaded(String info) {
display.clearDisplay();
display.setTextSize(1);
display.setCursor(25, 20);
display.print("Data Dimuat");
display.setCursor(25, 35);
display.print(info);
display.display();
}
void showProcessing() {
display.clearDisplay();
display.drawRect(0, 0, 128, 64, WHITE);
display.setTextSize(2);
display.setCursor(30, 5);
display.print("PROSES");
display.setTextSize(1);
display.setCursor(25, 25);
display.print("Mohon tunggu");
int centerX = 64;
int centerY = 45;
int radius = 10;
display.drawCircle(centerX, centerY, radius, WHITE);
static int angle = 0;
float rad = angle * 3.14159 / 180;
int dotX = centerX + radius * cos(rad);
int dotY = centerY + radius * sin(rad);
display.fillCircle(dotX, dotY, 2, WHITE);
angle = (angle + 45) % 360;
display.display();
}
void showInvalidCard() {
display.clearDisplay();
display.drawRect(0, 0, 128, 64, WHITE);
display.setTextSize(1);
display.setCursor(30, 15);
display.print("KARTU");
display.setTextSize(2);
display.setCursor(25, 28);
display.print("INVALID");
display.setTextSize(1);
display.setCursor(25, 50);
display.print("Coba lagi");
display.display();
}
// ============================================
// SHOW SUCCESS SHOLAT PRO - UPDATED dengan TOTAL POIN
// ============================================
void showSuccessSholatPro(String nama, String sholat, String status, int poin, int total_poin) {
// Slide 1: Nama dan Aktivitas (TIDAK BERUBAH)
display.clearDisplay();
display.fillRect(0, 0, 128, 14, WHITE);
display.setTextColor(BLACK);
display.setTextSize(1);
int berhasilX = (128 - (8 * 6)) / 2;
display.setCursor(berhasilX, 3);
display.print("BERHASIL");
display.setTextColor(WHITE);
display.drawLine(0, 15, 128, 15, WHITE);
// Nama (ukuran besar)
display.setTextSize(2);
display.setCursor(10, 20);
display.print(nama.substring(0, 10));
// Aktivitas/Sholat (ukuran besar)
display.setTextSize(2);
display.setCursor(10, 40);
display.print(sholat.substring(0, 8));
display.display();
successTone();
delay(2000);
// ============================================
// Slide 2: Status + Poin Didapat dan TOTAL POIN
// ============================================
display.clearDisplay();
// Header "STATUS" dengan latar putih
display.fillRect(0, 0, 128, 14, WHITE);
display.setTextColor(BLACK);
display.setTextSize(1);
int statusHeaderX = (128 - (6 * 6)) / 2;
display.setCursor(statusHeaderX, 3);
display.print("STATUS");
display.setTextColor(WHITE);
display.drawLine(0, 15, 128, 15, WHITE);
// Baris 1: Status dan Poin Didapat (seperti sebelumnya)
display.setTextSize(2);
String statusTampil = "";
if (status == "tepat_waktu" || status == "Tepat Waktu" || status == "tepat") {
statusTampil = "TEPAT";
}
else if (status == "telat" || status == "Terlambat" || status == "terlambat") {
statusTampil = "TELAT";
}
else if (status == "terlalu_cepat" || status == "cepat" || status == "Terlalu Cepat") {
statusTampil = "CEPAT";
}
else {
statusTampil = status.substring(0, 8);
}
// Format: "TEPAT +5"
String baris1 = statusTampil + " +" + String(poin);
int baris1Width = baris1.length() * 12;
int baris1X = (128 - baris1Width) / 2;
display.setCursor(baris1X, 22);
display.print(baris1);
// Baris 2: TOTAL POIN (BARU)
display.setTextSize(1);
String baris2 = "Poin : ";
if (total_poin >= 0) {
baris2 += String(total_poin);
} else {
baris2 += "--"; // Jika gagal ambil total poin
}
int baris2Width = baris2.length() * 6;
int baris2X = (128 - baris2Width) / 2;
display.setCursor(baris2X, 45);
display.print(baris2);
display.display();
delay(2000);
}
// ============================================
// SHOW SUCCESS KEGIATAN PRO - UPDATED dengan TOTAL POIN
// ============================================
void showSuccessKegiatanPro(String nama, String kegiatan, String status, int poin, String waktuMulai, String waktuSelesai, int total_poin) {
// Slide 1: Nama dan Aktivitas (TIDAK BERUBAH)
display.clearDisplay();
display.fillRect(0, 0, 128, 14, WHITE);
display.setTextColor(BLACK);
display.setTextSize(1);
int berhasilX = (128 - (8 * 6)) / 2;
display.setCursor(berhasilX, 3);
display.print("BERHASIL");
display.setTextColor(WHITE);
display.drawLine(0, 15, 128, 15, WHITE);
// Nama (ukuran besar)
display.setTextSize(2);
display.setCursor(10, 20);
display.print(nama.substring(0, 10));
// Kegiatan (ukuran besar)
display.setTextSize(2);
display.setCursor(10, 40);
if (kegiatan.length() > 8) {
display.print(kegiatan.substring(0, 8));
} else {
display.print(kegiatan);
}
display.display();
successTone();
delay(2000);
// ============================================
// Slide 2: Status + Poin Didapat dan TOTAL POIN
// ============================================
display.clearDisplay();
display.fillRect(0, 0, 128, 14, WHITE);
display.setTextColor(BLACK);
display.setTextSize(1);
int statusHeaderX = (128 - (6 * 6)) / 2;
display.setCursor(statusHeaderX, 3);
display.print("STATUS");
display.setTextColor(WHITE);
display.drawLine(0, 15, 128, 15, WHITE);
// Baris 1: Status dan Poin Didapat
display.setTextSize(2);
String statusTampil = "";
if (status == "hadir" || status == "Hadir") {
statusTampil = "HADIR";
} else if (status == "terlambat" || status == "Terlambat") {
statusTampil = "TERLAMBAT";
} else {
statusTampil = status.substring(0, 8);
}
// Format: "HADIR +5" (poin sesuai setting kegiatan)
String baris1 = statusTampil + " +" + String(poin);
int baris1Width = baris1.length() * 12;
int baris1X = (128 - baris1Width) / 2;
display.setCursor(baris1X, 22);
display.print(baris1);
// Baris 2: TOTAL POIN (BARU)
display.setTextSize(1);
String baris2 = "Poin : ";
if (total_poin >= 0) {
baris2 += String(total_poin);
} else {
baris2 += "--"; // Jika gagal ambil total poin
}
int baris2Width = baris2.length() * 6;
int baris2X = (128 - baris2Width) / 2;
display.setCursor(baris2X, 45);
display.print(baris2);
// Waktu kegiatan (kecil, opsional) - TETAP ADA
if (waktuMulai.length() > 0) {
display.setTextSize(1);
display.setCursor(5, 5);
display.print(waktuMulai.substring(11, 16) + "-" + waktuSelesai.substring(11, 16));
}
display.display();
delay(2000);
}
void showDuplicateSholatPro(String sholat, String waktu) {
display.clearDisplay();
display.fillRect(0, 0, 128, 64, WHITE);
display.setTextColor(BLACK);
display.setTextSize(1);
display.setCursor(35, 5);
display.print("SUDAH ABSEN");
display.drawLine(0, 15, 128, 15, BLACK);
display.setCursor(10, 22);
display.print("Sholat: ");
display.print(sholat);
display.setCursor(10, 35);
display.print("Terakhir absen pukul ");
if (waktu.length() > 10) display.print(waktu.substring(11, 16));
else display.print("--:--");
display.display();
duplicateTone();
delay(3000);
}
void showDuplicateKegiatanPro(String kegiatan, String waktu) {
display.clearDisplay();
display.fillRect(0, 0, 128, 64, WHITE);
display.setTextColor(BLACK);
display.setTextSize(1);
display.setCursor(35, 5);
display.print("SUDAH ABSEN");
display.drawLine(0, 15, 128, 15, BLACK);
display.setCursor(10, 22);
display.print("Kegiatan: ");
display.print(kegiatan.substring(0, 14));
display.setCursor(10, 38);
display.print("Terakhir absen pukul ");
if (waktu.length() > 10) display.print(waktu.substring(11, 16));
else display.print("--:--");
display.display();
duplicateTone();
delay(3000);
}
void showDitolakPro(String sholat, String kegiatan, String keterangan, String displayMsg) {
display.clearDisplay();
display.fillRect(0, 0, 128, 64, WHITE);
display.setTextColor(BLACK);
display.setTextSize(2);
display.setCursor(25, 5);
display.print("DITOLAK");
display.drawLine(0, 25, 128, 25, BLACK);
display.setTextSize(1);
if (kegiatan.length() > 0) {
display.setCursor(10, 30);
display.print("Kegiatan:");
display.setCursor(10, 40);
display.print(kegiatan.substring(0, 14));
} else if (sholat.length() > 0) {
display.setCursor(10, 30);
display.print("Sholat:");
display.setCursor(10, 40);
display.print(sholat);
} else {
display.setCursor(10, 35);
display.print(keterangan.substring(0, 16));
}
if (displayMsg.length() > 0) {
display.setCursor(10, 52);
display.print(displayMsg.substring(0, 14));
}
display.display();
warningTone();
delay(3000);
}
void showErrorPro(String title, String msg) {
display.clearDisplay();
display.drawRect(0, 0, 128, 64, WHITE);
display.setTextSize(1);
display.setCursor(45, 10);
display.print(title);
display.drawLine(20, 20, 108, 20, WHITE);
display.setCursor(20, 30);
display.print(msg);
display.drawLine(100, 5, 115, 20, WHITE);
display.drawLine(100, 20, 115, 5, WHITE);
display.display();
}
// ============================================
// FUNGSI UTILITY
// ============================================
void updateTime() {
String timeStr = timeClient.getFormattedTime();
unsigned long epochTime = timeClient.getEpochTime();
struct tm *ptm = gmtime((time_t *)&epochTime);
int year = ptm->tm_year + 1900;
int month = ptm->tm_mon + 1;
int day = ptm->tm_mday;
char buffer[20];
sprintf(buffer, "%04d-%02d-%02d %s", year, month, day, timeStr.c_str());
currentTimestamp = String(buffer);
}
void beep(int times) {
for (int i = 0; i < times; i++) {
playBuzzerTone(NOTE_C5, 100);
digitalWrite(LED_PIN, HIGH);
delay(100);
digitalWrite(LED_PIN, LOW);
if (i < times - 1) delay(100);
}
}
// ============================================
// FUNGSI AMBIL TOTAL POIN SANTRI
// ============================================
int getTotalPoinSantri(int peserta_id) {
if (WiFi.status() != WL_CONNECTED) {
Serial.println("WiFi offline, tidak bisa ambil total poin");
return -1;
}
String url = String("https://zidcreative.com/sishopint/api/get_total_poin.php?peserta_id=") + String(peserta_id);
HTTPClient http;
http.begin(url);
http.setTimeout(3000);
int httpCode = http.GET();
int total_poin = -1;
if (httpCode == 200) {
String response = http.getString();
JsonDocument doc;
DeserializationError error = deserializeJson(doc, response);
if (!error && doc["success"].as<bool>()) {
total_poin = doc["total_poin"].as<int>();
Serial.print("Total poin santri: ");
Serial.println(total_poin);
} else {
Serial.println("Gagal parse response total poin");
}
} else {
Serial.print("HTTP Error get total poin: ");
Serial.println(httpCode);
}
http.end();
return total_poin;
}