diff --git a/Arduino/iot-lari.ino b/Arduino/iot-lari.ino new file mode 100644 index 0000000..1a2ed6c --- /dev/null +++ b/Arduino/iot-lari.ino @@ -0,0 +1,1063 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#include "addons/TokenHelper.h" +#include "addons/RTDBHelper.h" + +#define FIREBASE_PROJECT_ID "ta-running" +#define API_KEY "AIzaSyDcIQatv2KWTh96025wlFNEbH3xH2MK88k" +#define DATABASE_URL "https://ta-running-default-rtdb.asia-southeast1.firebasedatabase.app/" + +#define USER_EMAIL "esp32@running.com" +#define USER_PASSWORD "35P32RF1D" + +#define SS_PIN 5 +#define RST_PIN 2 +#define BUZZER_PIN 4 +#define BUTTON_REG_PIN 26 +#define BUTTON_START_PIN 27 + +LiquidCrystal_I2C lcd(0x27, 16, 2); +MFRC522 mfrc522(SS_PIN, RST_PIN); + +FirebaseData fbdo; +FirebaseAuth auth; +FirebaseConfig config; + +String userUID = ""; +String getUID(); +void buzz(int durasi); +bool modeDaftarRFID = false; +unsigned long regStartTime = 0; +const unsigned long regTimeout = 10000; // 10 detik timeout registrasi + +bool modeScanUlang = false; +unsigned long waktuMulaiScanUlang = 0; +const unsigned long batasWaktuScanUlang = 60000; // 60 detik (kembali ke 60s untuk individual) + +bool pesanScanAktif = false; +unsigned long waktuPesanScanAktif = 0; + +bool pesanTungguUser = false; +unsigned long waktuPesanTungguUser = 0; + +int totalParticipantsInEvent = 0; // Untuk melacak total peserta yang terdaftar di event ini +int finishedParticipantsCount = 0; // Untuk melacak berapa banyak peserta yang sudah menyelesaikan lapnya + +// ===================================================================================== +// STRUCT DAN VARIABEL UNTUK MODE INDIVIDUAL (LOGIKA LAMA ANDA) +// ===================================================================================== +struct RunningUserIndividual { // Diubah namanya agar tidak bentrok + String uidTag, uidApk, type; + int putaranTarget, scanCount; + unsigned long startTime, lastScanTime; + bool finished; +}; + +struct PendingUserIndividual { // Diubah namanya agar tidak bentrok + String uidTag, uidApk, type; + int putaranTarget; + bool readyToStart; + unsigned long waktuMasuk; +}; + +std::vector runningUsersIndividual; // Diubah namanya +std::vector pendingUsersIndividual; // Diubah namanya +std::map cachedUIDsIndividual; // Diubah namanya + +// ===================================================================================== +// STRUCT DAN VARIABEL UNTUK MODE EVENT (LOGIKA BARU) +// ===================================================================================== +struct EventParticipant { + String uidTag; + int lapCount; // Jumlah lap yang sudah dicatat untuk peserta ini (termasuk lap_0) + unsigned long lapStartTime; // Waktu lap_0 dicatat (millis()) + unsigned long lastScanTime; // Waktu scan terakhir (millis()) + int putaranTarget; // Target putaran untuk peserta ini (diambil dari /activities jika ada) + bool finished; // Status selesai untuk peserta ini +}; +std::map eventParticipants; // Map UID Tag ke EventParticipant + +String currentEventId = ""; // ID event yang sedang aktif +unsigned long eventGlobalStartTime = 0; // Waktu event global dimulai +bool countdownActive = false; // Flag untuk countdown scan +bool eventStarted = false; // Flag event global sudah dimulai +bool waitingForGlobalStartButton = false; // Flag menunggu tombol START event global + +unsigned long waktuMulaiScan = 0; // Untuk countdown dan timeout registrasi individual + +// ===================================================================================== +// VARIABEL UMUM +// ===================================================================================== +unsigned long lastRFIDScanTime = 0; +const unsigned long rfidScanInterval = 200; + +unsigned long lastCheck = 0; +const unsigned long checkInterval = 5000; + +//button start (untuk mode individual) +bool lastStartState = HIGH; +int stableStartState = HIGH; +unsigned long lastDebounceTime = 0; +const unsigned long debounceDelay = 30; // Delay debounce (ms) +bool waitingForRelease = false; + +struct DeleteSchedule { + String uidTag; + unsigned long deleteTime; // waktu millis() untuk hapus +}; +std::vector deleteSchedules; + + +void setup() { + Serial.begin(115200); + lcd.init(); lcd.backlight(); + + WiFiManager wm; + wm.autoConnect("RunIoT_Setup", "12345678"); + lcd.clear(); lcd.print("WiFi Connected"); delay(1000); + + //Sinkronisasi waktu dari NTP + configTime(0, 0, "pool.ntp.org", "time.nist.gov"); + struct tm timeinfo; + if (!getLocalTime(&timeinfo)) { + Serial.println("❌ Gagal sinkron waktu NTP"); + } else { + Serial.println("✅ Waktu sinkron dari NTP"); + } + + //Setup Firebase + config.api_key = API_KEY; + config.database_url = DATABASE_URL; + auth.user.email = USER_EMAIL; + auth.user.password = USER_PASSWORD; + Firebase.begin(&config, &auth); + Firebase.reconnectWiFi(true); + lcd.clear(); lcd.print("Login Firebase"); delay(1000); + + while ((auth.token.uid) == "") { + Firebase.refreshToken(&config); + delay(500); + } + userUID = String(auth.token.uid.c_str()); + + SPI.begin(); mfrc522.PCD_Init(); + pinMode(BUZZER_PIN, OUTPUT); + pinMode(BUTTON_REG_PIN, INPUT_PULLUP); + pinMode(BUTTON_START_PIN, INPUT_PULLUP); + digitalWrite(BUZZER_PIN, LOW); + lcd.clear(); lcd.print("Sistem Ready"); +} + +// ===================================================================================== +// DEKLARASI FUNGSI +// ===================================================================================== +// Fungsi Umum +void buzz(int durasi); +String getUID(); +String getISOTime(); +void resetSystemState(); // Untuk event +void checkDeleteSchedules(); // Untuk kedua mode + +// Fungsi Mode Individual (Logika Asli Anda) +void handleRegistrationIndividual(); // Diubah namanya +void cekTombolStartIndividual(); // Diubah namanya +void cekFirebaseIndividual(); // Diubah namanya +void cekTimeoutScanUlangIndividual(); // Diubah namanya +void handleRFIDScanIndividual(); // Diubah namanya +void updateRunningUsersIndividual(); // Diubah namanya + +// Fungsi Mode Event (Logika Baru) +void listenToEventStatus(); +void startScanCountdownLogic(); +void handleStartButtonEvent(); // Untuk tombol START event global & lap_0 +void handleRFIDScanEvent(); // Untuk scan RFID saat event berjalan +void updateEventParticipants(); // Untuk mengelola eventParticipants map +void checkEventCompletion(); + +void loop() { + if (!Firebase.ready()) return; + + // Cek status event di Firebase (dari aplikasi superuser) + listenToEventStatus(); + + // ===================================================================================== + // LOGIKA UTAMA LOOP BERDASARKAN MODE + // ===================================================================================== + if (eventStarted || countdownActive || waitingForGlobalStartButton) { // MODE EVENT + // Handle tombol START untuk event (global start / lap_0) + handleStartButtonEvent(); + + if (countdownActive) { + startScanCountdownLogic(); + } else if (waitingForGlobalStartButton) { + lcd.setCursor(0, 0); + lcd.print("Tekan START Event"); + lcd.setCursor(0, 1); + lcd.print(" "); + } else if (eventStarted) { + // Tampilkan waktu berjalan event global + unsigned long elapsed = millis() - eventGlobalStartTime; + int sec = (elapsed / 1000) % 60; + int min = (elapsed / 60000) % 60; + int hr = (elapsed / 3600000); + + lcd.setCursor(0, 0); + lcd.print("Event: "); + if (hr < 10) lcd.print("0"); + lcd.print(hr); + lcd.print(":"); + if (min < 10) lcd.print("0"); + lcd.print(min); + lcd.print(":"); + if (sec < 10) lcd.print("0"); + lcd.print(sec); + lcd.print(" "); + } + + // Handle RFID Scan untuk event + handleRFIDScanEvent(); + // Update participant status (misal finished) + updateEventParticipants(); + checkEventCompletion(); + + } else { // MODE INDIVIDUAL (jika tidak ada event aktif) + handleRFIDScanIndividual(); + cekTombolStartIndividual(); + handleRegistrationIndividual(); + cekFirebaseIndividual(); + cekTimeoutScanUlangIndividual(); + updateRunningUsersIndividual(); + + // Tampilan LCD untuk mode individual + if (modeDaftarRFID) { + // Tampilan sudah di handleRegistrationIndividual + } else if (modeScanUlang) { + int sisa = (batasWaktuScanUlang - (millis() - waktuMulaiScanUlang)) / 1000; + lcd.setCursor(0, 1); + lcd.print("Sisa: "); lcd.print(sisa); lcd.print(" detik "); + } else if (pesanScanAktif && millis() - waktuPesanScanAktif > 2000) { + pesanScanAktif = false; + lcd.clear(); lcd.print("Scan ulang RFID"); + } else if (pesanTungguUser && millis() - waktuPesanTungguUser > 2000) { + pesanTungguUser = false; + lcd.clear(); lcd.print("Scan ulang RFID"); + } else if (runningUsersIndividual.empty() && pendingUsersIndividual.empty()) { + lcd.clear(); lcd.print("Sistem Ready"); + } + } + + // Fungsi umum yang berjalan di kedua mode + checkDeleteSchedules(); +} + +// ===================================================================================== +// FUNGSI UMUM (digunakan di kedua mode atau independen) +// ===================================================================================== +String getUID() { + String uid = ""; + for (byte i = 0; i < mfrc522.uid.size; i++) { + uid += String(mfrc522.uid.uidByte[i] < 0x10 ? "0" : ""); + uid += String(mfrc522.uid.uidByte[i], HEX); + } + uid.toUpperCase(); + return uid; +} + +String getISOTime() { + struct tm timeinfo; + if (!getLocalTime(&timeinfo)) { + Serial.println("❌ Gagal ambil waktu lokal dari NTP"); + return ""; + } + + char buffer[30]; + strftime(buffer, sizeof(buffer), "%Y-%m-%dT%H:%M:%SZ", &timeinfo); + return String(buffer); +} + +void buzz(int durasi) { + digitalWrite(BUZZER_PIN, HIGH); + delay(durasi); + digitalWrite(BUZZER_PIN, LOW); +} + +void resetSystemState() { + // Reset semua flag dan data untuk kedua mode + countdownActive = false; + eventStarted = false; + waitingForGlobalStartButton = false; + currentEventId = ""; + eventGlobalStartTime = 0; + eventParticipants.clear(); // Clear data event + + modeDaftarRFID = false; + modeScanUlang = false; + pesanScanAktif = false; + pesanTungguUser = false; + runningUsersIndividual.clear(); // Clear data individual + pendingUsersIndividual.clear(); // Clear data individual + cachedUIDsIndividual.clear(); // Clear data individual + + deleteSchedules.clear(); + lcd.clear(); + lcd.print("Sistem Ready"); + Serial.println("Sistem telah direset."); +} + +void checkDeleteSchedules() { + unsigned long now = millis(); + for (auto it = deleteSchedules.begin(); it != deleteSchedules.end();) { + if (now >= it->deleteTime) { + String path = "/activities/" + it->uidTag; // Path ini berlaku untuk individual + // Untuk event, kita tidak menghapus dari /activities, hanya dari eventParticipants map + // dan status di /activities akan diubah oleh aplikasi admin + if (Firebase.RTDB.deleteNode(&fbdo, path)) { + Serial.println("Hapus data done: " + it->uidTag); + // Hapus juga dari cachedUIDsIndividual + cachedUIDsIndividual.erase(it->uidTag); + } else { + Serial.println("Gagal hapus data done: " + it->uidTag + " - " + fbdo.errorReason()); + } + it = deleteSchedules.erase(it); + } else { + ++it; + } + } +} + +// ===================================================================================== +// FUNGSI MODE INDIVIDUAL (LOGIKA ASLI ANDA, DENGAN PENYESUAIAN NAMA) +// ===================================================================================== +void handleRegistrationIndividual() { + if (digitalRead(BUTTON_REG_PIN) == LOW && !modeDaftarRFID && pendingUsersIndividual.empty() && runningUsersIndividual.empty()) { + delay(50); + if (digitalRead(BUTTON_REG_PIN) == LOW) { + while (digitalRead(BUTTON_REG_PIN) == LOW); // tunggu tombol dilepas + modeDaftarRFID = true; + waktuMulaiScan = millis(); // Gunakan waktuMulaiScan untuk timeout registrasi + lcd.clear(); lcd.print("Scan Tag RFID"); + } + } + + if (modeDaftarRFID) { + if (millis() - waktuMulaiScan > 10000) { + lcd.clear(); lcd.print("Scan Timeout!"); + delay(1000); + lcd.clear(); lcd.print("Sistem Ready"); + modeDaftarRFID = false; + return; + } + + if (mfrc522.PICC_IsNewCardPresent() && mfrc522.PICC_ReadCardSerial()) { + String scannedUID = getUID(); + buzz(100); + lcd.clear(); lcd.print("Kirim ke RTDB"); + + if (Firebase.RTDB.setString(&fbdo, "/gelang", scannedUID)) { + lcd.clear(); lcd.print("Terkirim!"); + buzz(100); + delay(2000); + if (Firebase.ready()) { + if (Firebase.RTDB.deleteNode(&fbdo, "/gelang")) { + Serial.println("Node /gelang berhasil dihapus"); + } else { + Serial.println("Gagal hapus /gelang: " + fbdo.errorReason()); + } + } + } else { + Serial.println("Gagal kirim ke RTDB: " + fbdo.errorReason()); + lcd.clear(); lcd.print("Gagal kirim!"); + delay(2000); + } + lcd.clear(); lcd.print("Sistem Ready"); + modeDaftarRFID = false; + } + } +} + +void cekTombolStartIndividual() { + int reading = digitalRead(BUTTON_START_PIN); + + if (reading != stableStartState) { + lastDebounceTime = millis(); + stableStartState = reading; + } + + if ((millis() - lastDebounceTime) > debounceDelay) { + if (stableStartState == LOW && lastStartState == HIGH) { + Serial.println("Tombol START Individual ditekan"); + + if (modeScanUlang) { + if (!pesanScanAktif) { + lcd.clear(); lcd.print("Scan masih aktif"); + buzz(300); + pesanScanAktif = true; + waktuPesanScanAktif = millis(); + } + lastStartState = stableStartState; + return; + } + + bool adaYangBelumSiap = false; + unsigned long sekarang = millis(); + + for (auto& p : pendingUsersIndividual) { + if (!p.readyToStart && (sekarang - p.waktuMasuk < 60000)) { + adaYangBelumSiap = true; + break; + } + } + + if (adaYangBelumSiap) { + if (!pesanTungguUser) { + lcd.clear(); lcd.print("Tunggu user lain"); + buzz(300); + pesanTungguUser = true; + waktuPesanTungguUser = millis(); + } + lastStartState = stableStartState; + return; + } + + for (auto it = pendingUsersIndividual.begin(); it != pendingUsersIndividual.end();) { + if (!it->readyToStart && sekarang - it->waktuMasuk >= 60000) { + Serial.println("Timeout user (tidak scan ulang): " + it->uidTag); + it = pendingUsersIndividual.erase(it); + } else { + ++it; + } + } + + for (auto it = pendingUsersIndividual.begin(); it != pendingUsersIndividual.end();) { + if (it->readyToStart) { + Serial.println("Mulai aktivitas user: " + it->uidTag); + Firebase.RTDB.setString(&fbdo, "/activities/" + it->uidTag + "/status", "running"); + RunningUserIndividual ru = {it->uidTag, it->uidApk, it->type, it->putaranTarget, 0, millis(), 0, false}; + runningUsersIndividual.push_back(ru); + lcd.clear(); lcd.print("Aktivitas Dimulai"); + buzz(300); + it = pendingUsersIndividual.erase(it); + } else { + ++it; + } + } + } + lastStartState = stableStartState; + } +} + +void cekFirebaseIndividual() { + unsigned long currentMillis = millis(); + if (currentMillis - lastCheck < checkInterval) return; + lastCheck = currentMillis; + + bool foundWaitingUser = false; + + if (!Firebase.RTDB.getString(&fbdo, "/activities")) { + Serial.println("❌ Gagal ambil data string dari /activities: " + fbdo.errorReason()); + return; + } + + String rawJson = fbdo.stringData(); + FirebaseJson rootJson; + rootJson.setJsonData(rawJson); + + size_t len = rootJson.iteratorBegin(); + for (size_t i = 0; i < len; i++) { + String uidTag, nestedRaw; + int type; + + rootJson.iteratorGet(i, type, uidTag, nestedRaw); + if (uidTag == "" || uidTag.length() < 6) continue; + + FirebaseJsonData jsonData; + FirebaseJson nestedJson; + nestedJson.setJsonData(nestedRaw); + + String activityStatus, activityType, uid_apk; + int putaran = 0; + + if (nestedJson.get(jsonData, "status")) activityStatus = jsonData.stringValue; + if (nestedJson.get(jsonData, "type")) activityType = jsonData.stringValue; + if (nestedJson.get(jsonData, "uid_apk")) uid_apk = jsonData.stringValue; + if (activityType == "lintasan" && nestedJson.get(jsonData, "putaran")) + putaran = jsonData.intValue; + + if (activityStatus == "waiting_for_rfid") { + foundWaitingUser = true; + + bool alreadyPending = false; + for (auto& p : pendingUsersIndividual) { + if (p.uidTag == uidTag) { + alreadyPending = true; + break; + } + } + + if (!alreadyPending) { + PendingUserIndividual pu; + pu.uidTag = uidTag; + pu.uidApk = uid_apk; + pu.type = activityType; + pu.putaranTarget = putaran; + pu.readyToStart = false; + pu.waktuMasuk = millis(); + pendingUsersIndividual.push_back(pu); + + cachedUIDsIndividual[uidTag] = uid_apk; + + Serial.println("✅ Tambah pending user individual: " + uidTag + " / " + uid_apk); + lcd.clear(); lcd.print("Scan ulang RFID"); + } + } + } + rootJson.iteratorEnd(); + + if (foundWaitingUser && !modeScanUlang) { + modeScanUlang = true; + waktuMulaiScanUlang = millis(); + Serial.println("⏱ Mode Scan Ulang Individual dimulai"); + } +} + +void cekTimeoutScanUlangIndividual() { + if (!modeScanUlang) return; + + if (millis() - waktuMulaiScanUlang > batasWaktuScanUlang) { + Serial.println("⏳ Waktu scan ulang Individual habis"); + + bool adaYangScan = false; + for (auto& p : pendingUsersIndividual) { + if (p.readyToStart) { + adaYangScan = true; + break; + } + } + + if (!adaYangScan) { + bool masihAdaWaiting = false; + if (Firebase.RTDB.getJSON(&fbdo, "/activities")) { + FirebaseJson& result = fbdo.jsonObject(); + size_t len = result.iteratorBegin(); + for (size_t i = 0; i < len; i++) { + String key, ignore; + int type; + result.iteratorGet(i, type, key, ignore); + String status; + FirebaseJsonData jsonData; + result.get(jsonData, key + "/status"); + if (jsonData.stringValue == "waiting_for_rfid") { + masihAdaWaiting = true; + break; + } + } + result.iteratorEnd(); + } + + if (masihAdaWaiting) { + Serial.println("🔄 Tidak ada yang scan, reset timer Individual"); + waktuMulaiScanUlang = millis(); + return; + } else { + modeScanUlang = false; + lcd.clear(); lcd.print("Menunggu START"); + return; + } + } + + for (auto it = pendingUsersIndividual.begin(); it != pendingUsersIndividual.end();) { + if (!it->readyToStart) { + Serial.println("❌ User timeout (tak scan): " + it->uidTag); + it = pendingUsersIndividual.erase(it); + } else { + ++it; + } + } + + modeScanUlang = false; + lcd.clear(); lcd.print("Tekan START"); + buzz(500); + } +} + +void handleRFIDScanIndividual() { + if (millis() - lastRFIDScanTime < rfidScanInterval) return; + lastRFIDScanTime = millis(); + + if (!mfrc522.PICC_IsNewCardPresent() || !mfrc522.PICC_ReadCardSerial()) return; + + String scannedUID = getUID(); + String apk; + + if (cachedUIDsIndividual.count(scannedUID) > 0) { + apk = cachedUIDsIndividual[scannedUID]; + } else { + if (Firebase.RTDB.getString(&fbdo, "/activities/" + scannedUID + "/uid_apk")) { + apk = fbdo.stringData(); + cachedUIDsIndividual[scannedUID] = apk; + } else { + lcd.clear(); lcd.print("UID tidak valid"); + buzz(500); + return; + } + } + + if (modeScanUlang) { + for (auto& p : pendingUsersIndividual) { + if (!p.readyToStart && p.uidTag == scannedUID) { + p.readyToStart = true; + Serial.println("✅ Scan ulang OK untuk: " + p.uidApk); + buzz(100); + lcd.clear(); lcd.print("Scan OK: "); lcd.print(p.uidApk.substring(0,6)); + delay(300); + return; + } + } + lcd.clear(); lcd.print("Bukan user aktif"); + buzz(200); + return; + } + + if (!modeScanUlang && runningUsersIndividual.empty()) return; + + for (auto& user : runningUsersIndividual) { + if (!user.finished && user.uidApk == apk && millis() - user.lastScanTime > 500) { + Serial.println("📍 USER match: " + user.uidApk); + Serial.println("📍 SCAN LAP ke-" + String(user.scanCount + 1)); + user.scanCount++; + user.lastScanTime = millis(); + buzz(100); + lcd.setCursor(0, 1); + lcd.print("Scan ke-"); lcd.print(user.scanCount); lcd.print(" "); + + String lapPath = "/activities/" + user.uidTag + "/laps/lap_" + String(user.scanCount) + "/timestamp"; + String waktuSekarang = getISOTime(); + bool ok = Firebase.RTDB.setString(&fbdo, lapPath.c_str(), waktuSekarang.c_str()); + + if (ok) { + Serial.println("✅ Timestamp berhasil disimpan: " + waktuSekarang); + } else { + Serial.println("❌ Gagal simpan timestamp: " + fbdo.errorReason()); + } + + bool selesai = false; + if (user.type == "lintasan" && user.scanCount >= user.putaranTarget) selesai = true; + if (user.type == "non-lintasan" && user.scanCount >= 1) selesai = true; + + if (selesai) { + unsigned long durasi = millis() - user.startTime; + Firebase.RTDB.setInt(&fbdo, "/activities/" + user.uidTag + "/duration", durasi); + Firebase.RTDB.setString(&fbdo, "/activities/" + user.uidTag + "/status", "done"); + user.finished = true; + lcd.clear(); lcd.print("Selesai!"); buzz(800); + + DeleteSchedule ds; + ds.uidTag = user.uidTag; + ds.deleteTime = millis() + 30000; + deleteSchedules.push_back(ds); + } + } + } + mfrc522.PICC_HaltA(); + mfrc522.PCD_StopCrypto1(); +} + +void updateRunningUsersIndividual() { + bool adaAktif = false; + for (auto& user : runningUsersIndividual) { + if (!user.finished) { + adaAktif = true; + if (Firebase.RTDB.getString(&fbdo, "/activities/" + user.uidTag + "/status") && fbdo.stringData() == "done") { + user.finished = true; + continue; + } + + unsigned long elapsed = millis() - user.startTime; + int sec = (elapsed / 1000) % 60; + int min = (elapsed / 60000) % 60; + int hr = (elapsed / 3600000); + + lcd.setCursor(0, 0); + lcd.print("Waktu: "); + if (hr < 10) lcd.print("0"); + lcd.print(hr); + lcd.print(":"); + if (min < 10) lcd.print("0"); + lcd.print(min); + lcd.print(":"); + if (sec < 10) lcd.print("0"); + lcd.print(sec); + lcd.print(" "); + } + } + + if (!adaAktif && !runningUsersIndividual.empty()) { + lcd.clear(); + lcd.print("Semua selesai"); + buzz(800); + delay(2000); + lcd.clear(); + lcd.print("Sistem Ready"); + } + + runningUsersIndividual.erase(std::remove_if(runningUsersIndividual.begin(), runningUsersIndividual.end(), [](RunningUserIndividual & u) { + return u.finished; + }), runningUsersIndividual.end()); +} + +// ===================================================================================== +// FUNGSI MODE EVENT (LOGIKA BARU) +// ===================================================================================== +void listenToEventStatus() { + if (Firebase.RTDB.getJSON(&fbdo, "/event_status")) { + FirebaseJson &eventStatusJson = fbdo.jsonObject(); + + size_t len = eventStatusJson.iteratorBegin(); + if (len > 0) { + int type; + String key, value; + + eventStatusJson.iteratorGet(0, type, key, value); + eventStatusJson.iteratorEnd(); + + String incomingEventId = key; + + if (currentEventId != incomingEventId) { + currentEventId = incomingEventId; + eventStarted = false; + countdownActive = false; + waitingForGlobalStartButton = false; + eventParticipants.clear(); // Clear event participants for new event + Serial.println("Current Event ID set to: " + currentEventId); + } + + if (Firebase.RTDB.getJSON(&fbdo, "/event_status/" + currentEventId)) { + FirebaseJson &eventData = fbdo.jsonObject(); + FirebaseJsonData jsonData; + bool startedFromFirebase = false; + bool scanModeFromFirebase = false; + + if (eventData.get(jsonData, "started")) startedFromFirebase = jsonData.boolValue; + if (eventData.get(jsonData, "scan_mode")) scanModeFromFirebase = jsonData.boolValue; + + // Logika untuk memulai countdown (started: true, scan_mode: true) + if (startedFromFirebase && scanModeFromFirebase && !countdownActive && !eventStarted && !waitingForGlobalStartButton) { + countdownActive = true; + waktuMulaiScan = millis(); + lcd.clear(); lcd.print("Scan: "); + Serial.println("🔔 Countdown dimulai!"); + } + // Logika ketika countdown selesai (Firebase scan_mode: false, started: true) + else if (startedFromFirebase && !scanModeFromFirebase && !eventStarted && !countdownActive) { + waitingForGlobalStartButton = true; + countdownActive = false; + Serial.println("Countdown selesai, menunggu tombol START Event."); + } + // Logika untuk menghentikan event dari Firebase (started: false) + else if (!startedFromFirebase && (eventStarted || countdownActive || waitingForGlobalStartButton)) { + Serial.println("Event dihentikan dari Firebase, mereset sistem."); + resetSystemState(); + } + } else { + Serial.println("Gagal membaca detail event untuk " + currentEventId); + } + } else { + // Tidak ada event dalam /event_status, reset jika event sebelumnya aktif + if (currentEventId != "") { + Serial.println("Tidak ada event dalam /event_status, mereset sistem."); + resetSystemState(); + } + } + } +} + +void startScanCountdownLogic() { + unsigned long batasWaktuScan = 20000; + int sisaWaktu = (batasWaktuScan - (millis() - waktuMulaiScan)) / 1000; + + lcd.setCursor(0, 1); + lcd.print("Sisa: "); + if (sisaWaktu < 0) sisaWaktu = 0; + lcd.print(sisaWaktu); + lcd.print(" detik"); + + if (mfrc522.PICC_IsNewCardPresent() && mfrc522.PICC_ReadCardSerial()) { + String scannedUID = getUID(); + buzz(100); + lcd.setCursor(0, 0); + lcd.print("UID: "); + lcd.print(scannedUID); + + Firebase.RTDB.setBool(&fbdo, "/scan_result/" + currentEventId + "/" + scannedUID, true); + Serial.println("UID dikirim ke RTDB: /scan_result/" + currentEventId + "/" + scannedUID + ": true"); + + mfrc522.PICC_HaltA(); + mfrc522.PCD_StopCrypto1(); + } + + if (millis() - waktuMulaiScan >= batasWaktuScan) { + lcd.clear(); + lcd.print("Tekan START Event"); + lcd.setCursor(0, 1); + lcd.print(" "); + countdownActive = false; + waitingForGlobalStartButton = true; + + if (currentEventId != "") { + Firebase.RTDB.setBool(&fbdo, "/event_status/" + currentEventId + "/scan_mode", false); + Serial.println("Firebase /event_status/" + currentEventId + "/scan_mode diatur ke false."); + } + Serial.println("Countdown selesai, menunggu tombol START Event."); + } +} + +void handleStartButtonEvent() { + int startButtonState = digitalRead(BUTTON_START_PIN); + + if (startButtonState != lastStartState) { + lastDebounceTime = millis(); + } + + if ((millis() - lastDebounceTime) > debounceDelay) { + if (startButtonState == LOW && stableStartState == HIGH) { + Serial.println("handleStartButtonEvent() called."); + + if (waitingForGlobalStartButton && currentEventId != "") { + eventStarted = true; + waitingForGlobalStartButton = false; + eventGlobalStartTime = millis(); + lcd.clear(); lcd.print("Event Dimulai!"); + buzz(500); + Serial.println("✅ Event dimulai via tombol START."); + + // Reset hitungan peserta selesai dan total peserta setiap kali event dimulai + totalParticipantsInEvent = 0; + finishedParticipantsCount = 0; // Pastikan ini direset + + String scanResultPath = "/scan_result/" + currentEventId; + String currentTime = getISOTime(); + + if (Firebase.RTDB.getJSON(&fbdo, scanResultPath)) { + String rawJson = fbdo.jsonString(); + Serial.println("Raw JSON for scan_result: " + rawJson); + + rawJson.replace("{", ""); + rawJson.replace("}", ""); + rawJson.trim(); + + if (rawJson.length() == 0) { + Serial.println("Tidak ada UID di /scan_result/" + currentEventId); + } else { + int startPos = 0; + int endPos = 0; + while (endPos != -1) { + endPos = rawJson.indexOf(',', startPos); + String pair; + if (endPos == -1) { + pair = rawJson.substring(startPos); + } else { + pair = rawJson.substring(startPos, endPos); + } + pair.trim(); + + int colonPos = pair.indexOf(':'); + if (colonPos != -1) { + String uidTag = pair.substring(0, colonPos); + String value = pair.substring(colonPos + 1); + + uidTag.replace("\"", ""); + uidTag.trim(); + value.replace("\"", ""); + value.trim(); + value.toLowerCase(); + + Serial.println("Manually parsed UID: " + uidTag + ", Value: " + value); + + if (uidTag.length() > 0 && value == "true") { + EventParticipant ep; + ep.uidTag = uidTag; + ep.lapCount = 0; + ep.lapStartTime = 0; // Penting: Ini harus 0 di awal, akan diset saat Lap 0 discan + ep.lastScanTime = 0; + ep.finished = false; + + String activityPath = "/activities/" + uidTag; + if (Firebase.RTDB.getJSON(&fbdo, activityPath)) { + FirebaseJson &activityJson = fbdo.jsonObject(); + FirebaseJsonData tempJsonData; + if (activityJson.get(tempJsonData, "putaran")) { + ep.putaranTarget = tempJsonData.intValue; + Serial.println(" Putaran target dari /activities: " + String(ep.putaranTarget)); + } else { + ep.putaranTarget = 1; + Serial.println(" Putaran target tidak ditemukan, default ke 1."); + } + if (activityJson.get(tempJsonData, "type") && tempJsonData.stringValue == "non-lintasan") { + ep.putaranTarget = 1; + Serial.println(" Tipe non-lintasan, putaran target diatur ke 1."); + } + } else { + ep.putaranTarget = 1; + Serial.println("Warning: Gagal ambil putaranTarget untuk UID " + uidTag + ". Menggunakan default 1. Error: " + fbdo.errorReason()); + } + + eventParticipants[uidTag] = ep; + totalParticipantsInEvent++; // INI BARU: Tambahkan ke total peserta + Serial.println(" UID " + uidTag + " added to eventParticipants map. Current map size: " + String(eventParticipants.size()) + ". Total participants: " + String(totalParticipantsInEvent)); + + } else { + Serial.println("Manually parsed UID " + uidTag + " ignored (value not 'true' or UID empty)."); + } + } + startPos = endPos + 1; + if (endPos == -1) break; + } + } + Serial.println("Jumlah eventParticipants setelah loop: " + String(eventParticipants.size()) + ". Final total participants: " + String(totalParticipantsInEvent)); + } else { + Serial.println("❌ Gagal ambil data dari /scan_result/" + currentEventId + ": " + fbdo.errorReason()); + } + } + } + stableStartState = startButtonState; + } + lastStartState = startButtonState; +} + +void handleRFIDScanEvent() { + if (countdownActive || modeDaftarRFID || !eventStarted) return; + + if (millis() - lastRFIDScanTime < rfidScanInterval) return; + lastRFIDScanTime = millis(); + + if (!mfrc522.PICC_IsNewCardPresent() || !mfrc522.PICC_ReadCardSerial()) return; + + String scannedUID = getUID(); + Serial.println("RFID scanned in Event Mode: " + scannedUID); + + if (eventParticipants.count(scannedUID) > 0) { + Serial.println("Scanned UID " + scannedUID + " found in eventParticipants."); + EventParticipant& user = eventParticipants[scannedUID]; + + if (!user.finished && millis() - user.lastScanTime > 500) { + Serial.println("Processing scan for " + scannedUID + ". Current lapCount: " + String(user.lapCount)); + + if (user.lapCount == 0 && user.lapStartTime == 0) { // Ini adalah scan pertama peserta (Lap 0) + user.lapStartTime = millis(); + String currentTime = getISOTime(); + String lapPath = "/event_activity_data/" + currentEventId + "/" + user.uidTag + "/laps/lap_0/timestamp"; + + if (Firebase.RTDB.setString(&fbdo, lapPath, currentTime)) { + Serial.println("Lap 0 tercatat untuk " + user.uidTag + ": " + currentTime); + } else { + Serial.println("❌ Gagal mencatat Lap 0 untuk " + user.uidTag + ": " + fbdo.errorReason()); + } + lcd.setCursor(0, 1); + lcd.print("START: "); lcd.print(user.uidTag.substring(0,6)); + user.lastScanTime = millis(); + buzz(100); + + Serial.println("UID " + scannedUID + " (Lap 0) - Next expected lap is 1."); + + } else { // Ini adalah scan untuk Lap 1, Lap 2, dst. + user.lapCount++; + user.lastScanTime = millis(); + buzz(100); + lcd.setCursor(0, 1); + lcd.print("Lap ke-"); lcd.print(user.lapCount); lcd.print(" "); + + String lapPath = "/event_activity_data/" + currentEventId + "/" + user.uidTag + "/laps/lap_" + String(user.lapCount) + "/timestamp"; + String waktuSekarang = getISOTime(); + bool ok = Firebase.RTDB.setString(&fbdo, lapPath.c_str(), waktuSekarang.c_str()); + + if (ok) { + Serial.println("✅ Timestamp berhasil disimpan: " + waktuSekarang + " untuk Lap " + String(user.lapCount)); + } else { + Serial.println("❌ Gagal simpan timestamp: " + fbdo.errorReason() + " untuk Lap " + String(user.lapCount)); + } + + bool selesai = false; + if (user.putaranTarget > 0 && user.lapCount >= user.putaranTarget) { + selesai = true; + Serial.println("UID " + user.uidTag + " mencapai atau melebihi putaranTarget (" + String(user.putaranTarget) + "). Menandai selesai."); + } else { + Serial.println("UID " + user.uidTag + " belum mencapai putaranTarget (" + String(user.putaranTarget) + "). Lanjut."); + } + + if (selesai) { + unsigned long durasi = millis() - user.lapStartTime; + Firebase.RTDB.setInt(&fbdo, "/event_activity_data/" + currentEventId + "/" + user.uidTag + "/duration", durasi); + Serial.println("Durasi tercatat untuk " + user.uidTag + ": " + String(durasi) + " ms"); + + Firebase.RTDB.setString(&fbdo, "/activities/" + user.uidTag + "/status", "done"); + user.finished = true; + lcd.clear(); lcd.print("Selesai!"); buzz(800); + + finishedParticipantsCount++; // INI BARU: Inkrementasi hitungan peserta selesai + Serial.println("Finished participants count: " + String(finishedParticipantsCount) + " / " + String(totalParticipantsInEvent)); + + DeleteSchedule ds; + ds.uidTag = user.uidTag; + ds.deleteTime = millis() + 30000; + deleteSchedules.push_back(ds); + Serial.println("UID " + user.uidTag + " ditambahkan ke jadwal penghapusan."); + } + } + } else if (user.finished) { + lcd.setCursor(0, 1); + lcd.print("Sudah Selesai!"); + buzz(200); + Serial.println("Scanned UID " + scannedUID + " is already finished."); + } else if (millis() - user.lastScanTime <= 500) { + lcd.setCursor(0, 1); + lcd.print("Scan terlalu cpt!"); + buzz(50); + Serial.println("Scanned UID " + scannedUID + ": Scan terlalu cepat."); + } + } else { + lcd.setCursor(0, 1); + lcd.print("UID tidak valid!"); + Serial.println("UID tidak valid discan (Event Mode): " + scannedUID + ". Not found in eventParticipants."); + buzz(300); + } + mfrc522.PICC_HaltA(); + mfrc522.PCD_StopCrypto1(); + + // INI BARU: Panggil fungsi untuk memeriksa apakah event sudah selesai + checkEventCompletion(); +} + +void checkEventCompletion() { + if (eventStarted && totalParticipantsInEvent > 0 && finishedParticipantsCount >= totalParticipantsInEvent) { + Serial.println("Semua peserta telah menyelesaikan event!"); + eventStarted = false; // Hentikan event + currentEventId = ""; // Kosongkan ID event + eventParticipants.clear(); // Hapus semua peserta dari map + deleteSchedules.clear(); // Hapus jadwal penghapusan + + lcd.clear(); + lcd.print("Event Selesai!"); + lcd.setCursor(0, 1); + lcd.print("System Ready."); + buzz(1000); // Buzzer panjang untuk menandakan event selesai + Serial.println("Event diakhiri. System Ready."); + } +} + +void updateEventParticipants() { + // Hapus peserta yang sudah selesai dari map eventParticipants + for (auto it = eventParticipants.begin(); it != eventParticipants.end();) { + if (it->second.finished) { + it = eventParticipants.erase(it); + } else { + ++it; + } + } +}