// ============================================
// KODE ESP32 - Si-ShoPint (LCD 16x2 I2C VERSION)
// ============================================
// UI UPDATE: Tampilan sederhana dengan format:
// - Home: Nama Lembaga + Jam + WiFi
// - Success: BERHASIL! + Poin di kanan
// - Ditolak: DITOLAK! + Alasan jelas
// - Error: ERROR! + Keterangan
// - Splash: Si-ShoPint + by Innoiti
// ============================================
// PERANGKAT:
// - ESP32 30 pin (standar)
// - LCD 16x2 I2C (address 0x27 atau 0x3F)
// - MFRC522 RFID Module
// - Buzzer pasif dengan PWM
// ============================================
#include <WiFi.h>
#include <HTTPClient.h>
#include <SPI.h>
#include <MFRC522.h>
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <NTPClient.h>
#include <WiFiUdp.h>
#include <ArduinoJson.h>
// ============================================
// LCD CONFIG
// ============================================
#define LCD_COLUMNS 16
#define LCD_ROWS 2
#define LCD_ADDRESS 0x27 // Coba 0x27, jika tidak 0x3F
LiquidCrystal_I2C lcd(LCD_ADDRESS, LCD_COLUMNS, LCD_ROWS);
// ============================================
// HARDWARE PINS (ESP32 30 pin - AMAN)
// ============================================
#define RST_PIN 27 // RFID RST
#define SS_PIN 5 // RFID SS (SPI)
#define BUZZER_PIN 4 // Buzzer PWM
#define LED_PIN 16 // LED Indicator
// I2C untuk LCD: SDA=21, SCL=22 (default)
// SPI untuk RFID: MOSI=23, MISO=19, SCK=18 (default)
// ============================================
// BUZZER FREQUENCIES (Hz)
// ============================================
#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
// PWM Configuration
#define BUZZER_CHANNEL 0
#define BUZZER_RESOLUTION 8
// ============================================
// 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;
// ============================================
// VARIABEL
// ============================================
String currentTimestamp = "";
int dailySholatCount = 0;
int dailyKegiatanCount = 0;
bool wifiConnected = false;
unsigned long lastDisplayUpdate = 0;
int displayPage = 0;
bool cardDetected = false;
unsigned long cardProcessTime = 0;
// ============================================
// VARIABEL UNTUK SCROLLING
// ============================================
unsigned long lastScrollUpdate = 0;
int scrollPosition = 0;
const int SCROLL_INTERVAL = 300; // ms per gerakan
String displayText = "";
// ============================================
// FUNGSI PWM BUZZER
// ============================================
void initBuzzerPWM() {
ledcSetup(BUZZER_CHANNEL, 1000, BUZZER_RESOLUTION);
ledcAttachPin(BUZZER_PIN, BUZZER_CHANNEL);
ledcWrite(BUZZER_CHANNEL, 0);
}
void playBuzzerTone(int frequency, int duration) {
if (frequency <= 0) {
ledcWrite(BUZZER_CHANNEL, 0);
return;
}
ledcSetup(BUZZER_CHANNEL, frequency, BUZZER_RESOLUTION);
ledcAttachPin(BUZZER_PIN, BUZZER_CHANNEL);
int dutyCycle = (1 << (BUZZER_RESOLUTION - 1)) - 1;
ledcWrite(BUZZER_CHANNEL, dutyCycle);
delay(duration);
ledcWrite(BUZZER_CHANNEL, 0);
}
// ============================================
// TONE FUNCTIONS
// ============================================
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);
}
}
void cardDetectedTone() {
playBuzzerTone(NOTE_A5, 60);
delay(30);
playBuzzerTone(NOTE_A5, 60);
delay(30);
}
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);
}
}
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);
}
}
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);
}
}
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);
}
}
void wifiSuccessTone() {
playBuzzerTone(NOTE_E4, 100);
delay(20);
playBuzzerTone(NOTE_G4, 100);
delay(20);
playBuzzerTone(NOTE_C5, 200);
delay(20);
}
void wifiFailedTone() {
playBuzzerTone(NOTE_C4, 250);
delay(30);
playBuzzerTone(NOTE_C4, 250);
delay(30);
}
void duplicateTone() {
playBuzzerTone(NOTE_F4, 200);
delay(30);
playBuzzerTone(NOTE_F4, 200);
delay(30);
}
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 DISPLAY LCD 16x2 - UI BARU
// ============================================
// ----- UTILITY FUNCTIONS -----
String truncateText(String text, int maxLen = 10) {
if (text.length() <= maxLen) return text;
return text.substring(0, maxLen - 3) + "...";
}
void lcdPrintRight(int row, String text) {
int padding = 16 - text.length();
if (padding < 0) padding = 0;
lcd.setCursor(padding, row);
lcd.print(text);
}
// ----- 1. SPLASH SCREEN -----
void showSplashScreen() {
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Si-ShoPint");
lcd.setCursor(0, 1);
lcd.print("by Innoiti");
delay(2000);
}
// ----- 2. HOME SCREEN (STANDBY) WITH SCROLLING -----
void showHomeScreen() {
lcd.clear();
// ============================================
// BARIS 1: Nama Lembaga (SCROLLING)
// ============================================
String fullText = String(LEMBAGA_NAME);
// Jika teks pendek (<= 16 karakter), tampilkan statis
if (fullText.length() <= 16) {
lcd.setCursor(0, 0);
lcd.print(fullText);
} else {
// Tambahkan spasi di awal dan akhir untuk efek marquee
String paddedText = " " + fullText + " ";
// Update scroll position
unsigned long now = millis();
if (now - lastScrollUpdate >= SCROLL_INTERVAL) {
scrollPosition++;
// Reset ketika sudah mencapai akhir
if (scrollPosition > fullText.length() + 3) {
scrollPosition = 0;
}
lastScrollUpdate = now;
}
// Ambil 16 karakter dari posisi scroll
String displayText = "";
int maxLen = paddedText.length();
for (int i = 0; i < 16; i++) {
int idx = scrollPosition + i;
if (idx >= maxLen) {
idx = idx - maxLen;
}
displayText += paddedText.charAt(idx);
}
lcd.setCursor(0, 0);
lcd.print(displayText);
}
// ============================================
// BARIS 2: Jam + WiFi Indicator (TETAP)
// ============================================
String timeStr = currentTimestamp.substring(11, 16); // HH:MM
String wifiIcon = wifiConnected ? "W" : "X";
String line2 = timeStr + " " + wifiIcon;
lcd.setCursor(9, 1);
lcd.print(line2);
}
// ----- 3. SUCCESS SHOLAT -----
void showSuccessSholatPro(String nama, String sholat, String status, int poin, int total_poin) {
lcd.clear();
// Baris 1: Nama User (kiri) + Poin didapat (kanan)
lcd.setCursor(0, 0);
String namaUser = truncateText(nama, 10);
lcd.print(namaUser);
// Poin di kanan (format "+5")
String poinText = "+" + String(poin);
lcdPrintRight(0, poinText);
// Baris 2: Nama Sholat (kiri) + Total Poin (kanan)
lcd.setCursor(0, 1);
String sholatText = truncateText(sholat, 10);
lcd.print(sholatText);
// Total poin di kanan
String totalText = (total_poin >= 0) ? String(total_poin) : "--";
lcdPrintRight(1, totalText);
successTone();
delay(2500);
}
// ----- 4. SUCCESS KEGIATAN -----
void showSuccessKegiatanPro(String nama, String kegiatan, String status, int poin, String waktuMulai, String waktuSelesai, int total_poin) {
lcd.clear();
// Baris 1: Nama User (kiri) + Poin didapat (kanan)
lcd.setCursor(0, 0);
String namaUser = truncateText(nama, 10);
lcd.print(namaUser);
// Poin di kanan (format "+5")
String poinText = "+" + String(poin);
lcdPrintRight(0, poinText);
// Baris 2: Nama Kegiatan (kiri) + Total Poin (kanan)
lcd.setCursor(0, 1);
String kegiatanText = truncateText(kegiatan, 10);
lcd.print(kegiatanText);
// Total poin di kanan
String totalText = (total_poin >= 0) ? String(total_poin) : "--";
lcdPrintRight(1, totalText);
successTone();
delay(2500);
}
// ----- 5. DITOLAK (UNIFIED) -----
void showDitolakPro(String sholat, String kegiatan, String keterangan, String displayMsg) {
lcd.clear();
// Baris 1: "DITOLAK!"
lcd.setCursor(0, 0);
lcd.print("DITOLAK!");
// Baris 2: Alasan penolakan
lcd.setCursor(0, 1);
String reason = "";
// Prioritaskan displayMsg dari server
if (displayMsg.length() > 0) {
reason = displayMsg;
}
// Gunakan keterangan
else if (keterangan.length() > 0) {
reason = keterangan;
}
// Jika ada kegiatan/sholat, tampilkan
else if (kegiatan.length() > 0) {
reason = "Kegiatan: " + truncateText(kegiatan, 8);
} else if (sholat.length() > 0) {
reason = "Sholat: " + truncateText(sholat, 8);
} else {
reason = "Tidak valid";
}
// Potong jika terlalu panjang
if (reason.length() > 16) reason = reason.substring(0, 14) + "..";
lcd.print(reason);
warningTone();
delay(2500);
}
// ----- 6. INVALID CARD (DITOLAK) -----
void showInvalidCard() {
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("DITOLAK!");
lcd.setCursor(0, 1);
lcd.print("Kartu tidak valid");
invalidCardTone();
delay(2500);
}
// ----- 7. DUPLICATE SHOLAT (DITOLAK) -----
void showDuplicateSholatPro(String sholat, String waktu) {
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("DITOLAK!");
lcd.setCursor(0, 1);
String reason = "Sudah absen " + truncateText(sholat, 6);
if (waktu.length() > 10) {
reason += " " + waktu.substring(11, 16);
}
if (reason.length() > 16) reason = reason.substring(0, 14) + "..";
lcd.print(reason);
duplicateTone();
delay(2500);
}
// ----- 8. DUPLICATE KEGIATAN (DITOLAK) -----
void showDuplicateKegiatanPro(String kegiatan, String waktu) {
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("DITOLAK!");
lcd.setCursor(0, 1);
String reason = "Sudah absen " + truncateText(kegiatan, 6);
if (waktu.length() > 10) {
reason += " " + waktu.substring(11, 16);
}
if (reason.length() > 16) reason = reason.substring(0, 14) + "..";
lcd.print(reason);
duplicateTone();
delay(2500);
}
// ----- 9. ERROR SCREEN -----
void showErrorPro(String title, String msg) {
lcd.clear();
// Baris 1: "ERROR!"
lcd.setCursor(0, 0);
lcd.print("ERROR!");
// Baris 2: Keterangan error
lcd.setCursor(0, 1);
String errorMsg = msg;
if (errorMsg.length() > 16) errorMsg = errorMsg.substring(0, 14) + "..";
lcd.print(errorMsg);
errorTone();
delay(2500);
}
// ----- 10. LOADING / PROCESSING -----
void showLoadingScreen(String line1, String line2) {
lcd.clear();
lcd.setCursor(0, 0);
lcd.print(line1.substring(0, 16));
lcd.setCursor(0, 1);
static int dotCount = 0;
String dots = "";
for(int i = 0; i < dotCount; i++) dots += ".";
lcd.print(line2.substring(0, 16 - dots.length()));
lcd.print(dots);
dotCount = (dotCount % 3) + 1;
}
void showProcessing() {
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Memproses...");
lcd.setCursor(0, 1);
static int dotCount = 0;
for(int i = 0; i < dotCount; i++) lcd.print(".");
dotCount = (dotCount % 3) + 1;
}
// ----- 11. WIFI CONNECTION -----
void showConnectingScreen() {
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Menghubungkan...");
lcd.setCursor(0, 1);
static int dots = 0;
lcd.print("WiFi");
for(int i = 0; i < dots; i++) lcd.print(".");
dots = (dots % 3) + 1;
}
void showWiFiProgress(int attempts) {
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("WiFi Connect");
lcd.setCursor(0, 1);
int progress = (attempts * 100) / 30;
lcd.print(progress);
lcd.print("%");
int barLen = (progress * 16) / 100;
for(int i = 0; i < barLen; i++) lcd.print("=");
}
void showWiFiSuccess() {
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("WiFi OK!");
lcd.setCursor(0, 1);
lcd.print(WiFi.localIP().toString());
delay(1500);
}
void showOfflineMode() {
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("MODE OFFLINE");
lcd.setCursor(0, 1);
lcd.print("Terbatas");
delay(1500);
}
void showDataLoaded(String info) {
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Data Dimuat");
lcd.setCursor(0, 1);
lcd.print(info.substring(0, 16));
delay(1000);
}
// ============================================
// 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();
} else {
wifiConnected = false;
Serial.println("\nWiFi GAGAL");
wifiFailedTone();
showOfflineMode();
}
}
void checkWiFi() {
if (WiFi.status() == WL_CONNECTED) {
if (!wifiConnected) {
wifiConnected = true;
Serial.println("WiFi Reconnected");
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");
showDataLoaded(String(jumlahKegiatan) + " kegiatan");
} else {
Serial.println("Gagal parse jadwal kegiatan");
showErrorPro("Parse Error", "Gagal parse");
}
} else {
Serial.print("HTTP Error: ");
Serial.println(httpCode);
showErrorPro("HTTP Error", String(httpCode));
}
http.end();
lastKegiatanUpdate = millis();
}
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");
showInvalidCard();
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");
showErrorPro("WiFi", "Putus");
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");
showErrorPro("Server", "Error");
}
http.end();
}
// ============================================
// FUNGSI PROCESS RESPONSE
// ============================================
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());
showErrorPro("Respon", "Error");
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;
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) {
showSuccessKegiatanPro(nama, namaKegiatanRes, displayMsg, poin, waktuMulai, waktuSelesai, total_poin);
} else {
String statusText = (statusPresensi == "hadir") ? "Hadir" : "Terlambat";
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) {
showSuccessSholatPro(nama, sholat, displayMsg, poin, total_poin);
} else {
showSuccessSholatPro(nama, sholat, statusPresensi, poin, total_poin);
}
}
} else {
handleErrorPro(errorCode, doc, namaKegiatan);
}
}
// ============================================
// 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") {
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") {
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") {
showErrorPro("Kegiatan", "Tidak ditemukan");
} else if (errorCode == "KEGIATAN_INACTIVE") {
showErrorPro("Kegiatan", "Non-aktif");
} else {
String msg = doc["message"] | "Error";
showErrorPro("Error", msg.substring(0, 12));
}
}
// ============================================
// 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);
}
}
}
void kontrolBuzzerPro(String buzzerType) {
if (buzzerType == "SHORT_BEEP") {
playBuzzerTone(NOTE_A5, 100);
} else if (buzzerType == "LONG_BEEP") {
playBuzzerTone(NOTE_G4, 600);
} else if (buzzerType == "DOUBLE_BEEP") {
playBuzzerTone(NOTE_C5, 100);
delay(20);
playBuzzerTone(NOTE_E5, 100);
} else if (buzzerType == "CONTINUOUS") {
for(int i = 0; i < 3; i++) {
playBuzzerTone(NOTE_C5, 200);
delay(20);
playBuzzerTone(NOTE_G5, 200);
delay(20);
}
}
}
// ============================================
// 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;
}
// ============================================
// 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);
}
// ============================================
// SETUP
// ============================================
void setup() {
Serial.begin(115200);
Serial.println("\n=== Si-ShoPint v2.0 (LCD 16x2 Version) ===");
// Init LCD
lcd.init();
lcd.backlight();
lcd.clear();
// 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();
showSplashScreen();
// Connect WiFi
connectWiFi();
// Init time
timeClient.begin();
timeClient.update();
updateTime();
// Ambil jadwal kegiatan dari server
if (wifiConnected) {
ambilJadwalKegiatan();
} else {
showOfflineMode();
}
startupTone();
}
// ============================================
// LOOP
// ============================================
void loop() {
static unsigned long lastTimeUpdate = 0;
static unsigned long lastWifiCheck = 0;
static unsigned long lastScreenUpdate = 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;
}
// Update home screen setiap 200ms (untuk animasi)
if (now - lastScreenUpdate >= 200) {
showHomeScreen();
lastScreenUpdate = now;
}
// Check RFID (non-blocking)
if (mfrc522.PICC_IsNewCardPresent() && mfrc522.PICC_ReadCardSerial()) {
cardDetectedTone();
processCard();
mfrc522.PICC_HaltA();
mfrc522.PCD_StopCrypto1();
}
delay(50);
}