364 lines
12 KiB
C++
364 lines
12 KiB
C++
#include <SPI.h>
|
|
#include <MFRC522.h>
|
|
#include "HX711.h"
|
|
#include <WiFi.h>
|
|
#include <Firebase_ESP_Client.h>
|
|
#include "time.h"
|
|
|
|
// ================= WIFI =================
|
|
#define WIFI_SSID "MalaikatIzrail"
|
|
#define WIFI_PASSWORD "12345678910"
|
|
|
|
// ================= FIREBASE =================
|
|
#define API_KEY "AIzaSyDhFtqvaTMgYH9vyAHFclDUXiOxkl-2GqM"
|
|
#define DATABASE_URL "https://silat-b3100-default-rtdb.firebaseio.com/"
|
|
|
|
#define USER_EMAIL "faniferdiyanto1980@gmail.com"
|
|
#define USER_PASSWORD "faniferdiyanto80"
|
|
|
|
// ================= PIN =================
|
|
#define SS_PIN 21
|
|
#define RST_PIN 22
|
|
#define DT_PIN 32
|
|
#define SCK_PIN 33
|
|
|
|
MFRC522 rfid(SS_PIN, RST_PIN);
|
|
HX711 scale;
|
|
|
|
// ================= FIREBASE =================
|
|
FirebaseData fbdo;
|
|
FirebaseData fbdoStream; // Dedicated stream object for test_command
|
|
FirebaseAuth auth;
|
|
FirebaseConfig config;
|
|
bool firebaseReady = false;
|
|
|
|
// ================= VARIABEL =================
|
|
float calibration_factor = 17740.0;
|
|
float offset_manual = 0.10;
|
|
float alpha = 0.95; // Ditingkatkan agar merespon lebih cepat / instan
|
|
float threshold = 2.5; // PERBAIKAN: Dinaikkan ke 2.5 KG agar lebih aman dari noise dan pergeseran mekanis
|
|
float smoothedWeight = 0;
|
|
float peakValue = 0;
|
|
|
|
// Test variables
|
|
String currentTestUID = "";
|
|
int currentAttempt = 1;
|
|
String currentStrikeType = "pukulan"; // Default
|
|
bool isTesting = false;
|
|
unsigned long testStartTime = 0;
|
|
unsigned long firstHitTime = 0; // Waktu saat pukulan pertama dideteksi
|
|
String handledCommand = ""; // Menghindari pengulangan perintah yang sama
|
|
const unsigned long TEST_DURATION = 5000;
|
|
|
|
float graphData[100];
|
|
int graphIndex = 0;
|
|
|
|
// Timer Optimasi
|
|
unsigned long lastFirebaseUpdate = 0;
|
|
const unsigned long FIREBASE_UPDATE_INTERVAL = 300; // Lebih cepat: 100ms
|
|
unsigned long lastSampleTime = 0;
|
|
const unsigned long SAMPLING_INTERVAL = 30; // Lebih cepat: 30ms
|
|
|
|
// PERBAIKAN: Throttle checkTestCommand agar tidak blocking setiap loop
|
|
unsigned long lastCommandCheck = 0;
|
|
const unsigned long COMMAND_CHECK_INTERVAL = 500; // Cek perintah setiap 500ms saja
|
|
|
|
// ================= DATA LOGGING =================
|
|
String getUID() {
|
|
String uid = "";
|
|
for (byte i = 0; i < rfid.uid.size; i++) {
|
|
if (rfid.uid.uidByte[i] < 0x10) uid += "0";
|
|
uid += String(rfid.uid.uidByte[i], HEX);
|
|
}
|
|
uid.toUpperCase();
|
|
return uid;
|
|
}
|
|
|
|
void syncTime() {
|
|
configTime(0, 0, "pool.ntp.org", "time.nist.gov", "time.google.com");
|
|
Serial.print("Sync time");
|
|
time_t now = time(nullptr);
|
|
int attempts = 0;
|
|
while (now < 100000 && attempts < 40) {
|
|
delay(500);
|
|
Serial.print(".");
|
|
now = time(nullptr);
|
|
attempts++;
|
|
}
|
|
if (now >= 100000) Serial.println("\n✅ Time synchronized");
|
|
else Serial.println("\n⚠️ Time sync failed");
|
|
}
|
|
|
|
void initFirebase() {
|
|
config.api_key = API_KEY;
|
|
config.database_url = DATABASE_URL;
|
|
auth.user.email = USER_EMAIL;
|
|
auth.user.password = USER_PASSWORD;
|
|
config.timeout.serverResponse = 10000;
|
|
|
|
// PERBAIKAN: Memperbesar buffer TX menjadi 4096 byte agar mampu menampung paket JSON grafik sekaligus!
|
|
fbdo.setBSSLBufferSize(4096, 4096);
|
|
fbdo.setResponseSize(2048);
|
|
fbdoStream.setBSSLBufferSize(2048, 512);
|
|
|
|
Firebase.begin(&config, &auth);
|
|
Firebase.reconnectWiFi(true);
|
|
Serial.println("🔥 Firebase Initialized");
|
|
}
|
|
|
|
void updateSensorStatus() {
|
|
if (firebaseReady) {
|
|
// Tulis Unix timestamp agar dashboard bisa deteksi kapan terakhir aktif
|
|
time_t now = time(nullptr);
|
|
Firebase.RTDB.setString(&fbdo, "/system/sensor_status", "Online");
|
|
Firebase.RTDB.setInt(&fbdo, "/system/last_seen", (int)now);
|
|
}
|
|
}
|
|
|
|
void handleRFIDScan() {
|
|
if (!rfid.PICC_IsNewCardPresent() || !rfid.PICC_ReadCardSerial()) return;
|
|
String uid = getUID();
|
|
Serial.println("RFID detected: " + uid);
|
|
Firebase.RTDB.setString(&fbdo, "/system/last_scanned", uid);
|
|
delay(1000);
|
|
rfid.PICC_HaltA();
|
|
rfid.PCD_StopCrypto1();
|
|
}
|
|
|
|
float kgToNewton(float kg) { return kg * 9.81; }
|
|
|
|
void checkTestCommand() {
|
|
if (isTesting) return;
|
|
|
|
if (millis() - lastCommandCheck < COMMAND_CHECK_INTERVAL) return;
|
|
lastCommandCheck = millis();
|
|
|
|
if (Firebase.RTDB.get(&fbdoStream, "/test_command")) {
|
|
String command = fbdoStream.stringData();
|
|
|
|
// PERBAIKAN: Hanya jalan jika perintah baru (cegah loop otomatis)
|
|
if (command != "" && command != "null" && command != handledCommand && command.indexOf('|') > 0) {
|
|
handledCommand = command; // Tandai perintah sudah ditangani
|
|
|
|
int firstSep = command.indexOf('|');
|
|
int secondSep = command.indexOf('|', firstSep + 1);
|
|
|
|
currentTestUID = command.substring(0, firstSep);
|
|
|
|
if (secondSep > 0) {
|
|
currentAttempt = command.substring(firstSep + 1, secondSep).toInt();
|
|
currentStrikeType = command.substring(secondSep + 1);
|
|
} else {
|
|
currentAttempt = command.substring(firstSep + 1).toInt();
|
|
currentStrikeType = "pukulan"; // Fallback
|
|
}
|
|
|
|
Serial.println("--- TEST STARTED ---");
|
|
Serial.print("UID: "); Serial.println(currentTestUID);
|
|
Serial.print("Attempt: "); Serial.println(currentAttempt);
|
|
Serial.print("Type: "); Serial.println(currentStrikeType);
|
|
|
|
isTesting = true;
|
|
testStartTime = millis();
|
|
firstHitTime = 0;
|
|
peakValue = 0;
|
|
smoothedWeight = 0;
|
|
graphIndex = 0;
|
|
memset(graphData, 0, sizeof(graphData));
|
|
|
|
Firebase.RTDB.setFloat(&fbdo, "/realtime/current_force", 0);
|
|
Firebase.RTDB.setFloat(&fbdo, "/realtime/peak_force", 0);
|
|
Firebase.RTDB.setString(&fbdo, "/system/test_status", "measuring");
|
|
Firebase.RTDB.setString(&fbdo, "/test_command", ""); // Bersihkan perintah
|
|
|
|
// PERBAIKAN: Reset angka di layar dan nol-kan sensor tepat sebelum mulai
|
|
FirebaseJson rtReset;
|
|
rtReset.set("current_force", 0);
|
|
rtReset.set("peak_force", 0);
|
|
Firebase.RTDB.updateNode(&fbdo, "/realtime", &rtReset);
|
|
|
|
scale.tare(); // Nol-kan sensor untuk menghilangkan drift dari pukulan sebelumnya
|
|
}
|
|
}
|
|
}
|
|
|
|
void sendRealtimeData() {
|
|
if (!isTesting || !firebaseReady) return;
|
|
|
|
if (millis() - lastFirebaseUpdate > FIREBASE_UPDATE_INTERVAL) {
|
|
lastFirebaseUpdate = millis();
|
|
|
|
float newton = kgToNewton(smoothedWeight);
|
|
float peakNewton = kgToNewton(peakValue);
|
|
|
|
// MENGGUNAKAN Async DENGAN UPDATE NODE UNTUK MENGURANGI SPAM (Mencegah SSL Crash)
|
|
FirebaseJson rtJson;
|
|
rtJson.set("current_force", newton);
|
|
rtJson.set("peak_force", peakNewton);
|
|
Firebase.RTDB.updateNodeAsync(&fbdo, "/realtime", &rtJson);
|
|
|
|
// PENGIRIMAN GRAFIK REALTIME DIHAPUS:
|
|
// Mengirim titik grafik satu-per-satu setiap 300ms ke Firebase membebani memori SSL (menyebabkan Error mRunUntil).
|
|
// Grafik akan langsung dikirim sekaligus dalam format JSON pada saat SaveResult (jauh lebih cepat & aman).
|
|
}
|
|
}
|
|
|
|
void saveTestResult() {
|
|
if (!firebaseReady) return;
|
|
float peakNewton = kgToNewton(peakValue);
|
|
|
|
// Jika diberhentikan manual / tidak ada benturan
|
|
if (peakValue <= threshold) {
|
|
Serial.println("⚠️ No impact detected, skipping save.");
|
|
// Gunakan JSON agar reset dikirim dalam 1x request cepat (mencegah error SSL)
|
|
FirebaseJson resetJson;
|
|
resetJson.set("realtime/current_force", 0);
|
|
resetJson.set("realtime/peak_force", 0);
|
|
resetJson.set("system/test_status", "idle");
|
|
resetJson.set("test_command", "");
|
|
|
|
// PERBAIKAN: Hanya bersihkan memori Anti-Spam JIKA perintah reset berhasil dikirim ke Firebase!
|
|
// Ini mencegah "tombol mulai ngeklik sendiri" jika internet sempat putus.
|
|
if (Firebase.RTDB.updateNode(&fbdo, "/", &resetJson)) {
|
|
handledCommand = "";
|
|
} else {
|
|
Serial.println("❌ Gagal mereset status, mencegah infinite loop.");
|
|
}
|
|
return;
|
|
}
|
|
|
|
Serial.println("--- SAVING RESULT ---");
|
|
String historyPath = "/test_history/" + currentTestUID + "/attempt_" + String(currentAttempt);
|
|
|
|
// 1. Gabungkan semua data histori dalam 1 Paket JSON!
|
|
FirebaseJson historyJson;
|
|
historyJson.set("peak_kg", peakValue);
|
|
historyJson.set("peak_newton", peakNewton);
|
|
historyJson.set("type", currentStrikeType);
|
|
historyJson.set("timestamp", millis());
|
|
|
|
FirebaseJson graphJson;
|
|
for (int i = 0; i < graphIndex && i < 100; i++) {
|
|
graphJson.set(String(i), graphData[i]);
|
|
}
|
|
historyJson.set("graph", graphJson);
|
|
Firebase.RTDB.setJSON(&fbdo, historyPath, &historyJson);
|
|
|
|
// 2. Tampilkan hasil akhir di layar besar (Persistence) ditangani oleh Website
|
|
// menggunakan data dari histori di atas, sehingga kita tidak perlu kirim ulang ke realtime/current_force
|
|
// hal ini mencegah munculnya "puncak ganda" di grafik.
|
|
|
|
// 3. Reset Status Sistem
|
|
FirebaseJson statusReset;
|
|
statusReset.set("system/test_status", "idle");
|
|
statusReset.set("test_command", "");
|
|
|
|
if (Firebase.RTDB.updateNode(&fbdo, "/", &statusReset)) {
|
|
handledCommand = "";
|
|
}
|
|
Serial.println("✅ Result Saved!");
|
|
}
|
|
|
|
void readLoadcell() {
|
|
if (!scale.is_ready()) return;
|
|
|
|
float raw = scale.get_units(1);
|
|
raw = raw + offset_manual; // Tambahkan hasil offset kalibrasi Anda
|
|
|
|
if (raw < 0) raw = 0; // Abaikan minus
|
|
|
|
// PERBAIKAN: Potong semua getaran ringan (di bawah 1.5 KG) menjadi 0.
|
|
// Ini mencegah munculnya angka "1" atau "2" Newton di layar akibat noise sensor!
|
|
if (raw < threshold) {
|
|
raw = 0;
|
|
}
|
|
|
|
// KUNCI RESPONSIVITAS: Gunakan data mentah (raw) untuk mendeteksi puncak, bukan data yang sudah dihaluskan!
|
|
if (raw > peakValue) peakValue = raw;
|
|
|
|
smoothedWeight = (alpha * raw) + ((1 - alpha) * smoothedWeight);
|
|
if (smoothedWeight < threshold) smoothedWeight = 0;
|
|
|
|
if (isTesting && millis() - lastSampleTime > SAMPLING_INTERVAL) {
|
|
lastSampleTime = millis();
|
|
if (graphIndex < 100) {
|
|
graphData[graphIndex] = kgToNewton(raw); // Gunakan raw juga agar grafik tidak terpotong (halus akan menahan data sejenak)
|
|
graphIndex++;
|
|
}
|
|
}
|
|
|
|
if (isTesting) {
|
|
sendRealtimeData();
|
|
|
|
// 1. Deteksi Pukulan Pertama (Auto-Stop Trigger)
|
|
if (raw > threshold && firstHitTime == 0) {
|
|
firstHitTime = millis();
|
|
Serial.println("🎯 Impact detected! Auto-stop in 3 seconds...");
|
|
}
|
|
|
|
// 2. Cek Auto-Stop (3 detik setelah pukulan)
|
|
if (firstHitTime > 0 && (millis() - firstHitTime > 3000)) {
|
|
Serial.println("⏹️ Auto-stop triggered");
|
|
isTesting = false;
|
|
saveTestResult();
|
|
firstHitTime = 0;
|
|
}
|
|
// 3. PERBAIKAN: Ganti pengecekan tombol Stop manual dengan Timeout otomatis (30 detik)
|
|
// Ini menghilangkan proses "blocking" saat mengambil data dari Firebase yang bikin Pukulan Delay!
|
|
else if (firstHitTime == 0 && (millis() - testStartTime > 30000)) {
|
|
Serial.println("⏳ Timeout, no impact detected.");
|
|
isTesting = false;
|
|
saveTestResult();
|
|
}
|
|
}
|
|
}
|
|
|
|
void setup() {
|
|
Serial.begin(115200);
|
|
delay(1000);
|
|
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
|
|
while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); }
|
|
|
|
syncTime();
|
|
initFirebase();
|
|
while (!Firebase.ready()) delay(100);
|
|
firebaseReady = true;
|
|
|
|
Firebase.RTDB.setString(&fbdo, "/system/test_status", "idle");
|
|
Firebase.RTDB.setString(&fbdo, "/test_command", "");
|
|
|
|
SPI.begin();
|
|
rfid.PCD_Init();
|
|
|
|
// PERBAIKAN: Kekuatan antena RFID
|
|
// Disetel ke 38dB sesuai hasil pengetesan. Level ini paling stabil dan responsif
|
|
// untuk menembus kotak plastik 2mm tanpa membuat modul RC522 crash/blank.
|
|
rfid.PCD_SetAntennaGain(rfid.RxGain_38dB);
|
|
|
|
scale.begin(DT_PIN, SCK_PIN);
|
|
delay(2000);
|
|
|
|
// Set kalibrasi dengan angka yang sudah fix
|
|
scale.set_scale(calibration_factor);
|
|
scale.tare();
|
|
|
|
Serial.println("🚀 SYSTEM READY");
|
|
}
|
|
|
|
void loop() {
|
|
if (Firebase.ready()) {
|
|
readLoadcell();
|
|
checkTestCommand();
|
|
|
|
static unsigned long lastStatusUpdate = 0;
|
|
if (millis() - lastStatusUpdate > 10000) {
|
|
lastStatusUpdate = millis();
|
|
updateSensorStatus();
|
|
}
|
|
|
|
if (!isTesting) {
|
|
handleRFIDScan();
|
|
}
|
|
}
|
|
}
|