E32221324_Iot_Running/Arduino/iot-lari.ino

1064 lines
37 KiB
C++

#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <SPI.h>
#include <MFRC522.h>
#include <Firebase_ESP_Client.h>
#include <WiFiManager.h>
#include <vector>
#include <map>
#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<RunningUserIndividual> runningUsersIndividual; // Diubah namanya
std::vector<PendingUserIndividual> pendingUsersIndividual; // Diubah namanya
std::map<String, String> 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<String, EventParticipant> 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<DeleteSchedule> 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;
}
}
}