1064 lines
37 KiB
C++
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;
|
|
}
|
|
}
|
|
}
|