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