#include "HX711.h" #include #include #include #include #include #include #include #include "addons/RTDBHelper.h" #include "addons/TokenHelper.h" // KONFIGURASI FIREBASE #define API_KEY "AIzaSyDtb-5Qs13elSodm8gRJ-rGj0kornwd1BY" #define DATABASE_URL "mylaundry-d02fb-default-rtdb.asia-southeast1.firebasedatabase.app" // PIN HARDWARE #define SCREEN_WIDTH 128 #define SCREEN_HEIGHT 64 #define OLED_RESET -1 #define I2C_ADDRESS 0x3C #define LOADCELL_DOUT_PIN 32 #define LOADCELL_SCK_PIN 33 #define BUTTON_PIN 27 #define BUZZER_PIN 25 // OBJEK GLOBAL Adafruit_SH1106G display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET); HX711 scale; float calibration_factor = 103500.00; Preferences preferences; FirebaseData fbdo; FirebaseData streamDo; FirebaseAuth auth; FirebaseConfig config; // BERAT REALTIME & FITUR LOCK float currentWeight = 0.000; bool isLocked = false; float lockedWeight = 0.000; float lastWeight = 0.000; unsigned long stableStartTime = 0; // PENGATURAN SENSITIVITAS LOCK const float TOLERANSI_STABIL = 0.005f; const float TOLERANSI_UNLOCK = 0.02f; const unsigned long WAKTU_STABIL = 2000; // SERIAL MONITOR unsigned long lastSerialTime = 0; #define SERIAL_PRINT_INTERVAL 500 // STATE DISPLAY String strJasa = "-"; String strEst = "-"; String strTipeLayanan = "-"; String strBerat = "0.000"; String strHarga = "Rp0"; // FIREBASE PATH String firebasePath = "/laundry"; // TEXT int scrollPosX_Jasa = 42; int scrollPosX_Berat = 42; unsigned long lastScrollTime = 0; // STREAM HEALTH CHECK unsigned long lastStreamCheck = 0; #define STREAM_CHECK_INTERVAL 30000 void beepBuzzer(int times) { for (int i = 0; i < times; i++) { digitalWrite(BUZZER_PIN, HIGH); delay(150); digitalWrite(BUZZER_PIN, LOW); delay(100); } } String getValue(String data, char separator, int index) { int found = 0; int strIndex[] = {0, -1}; int maxIndex = data.length() - 1; for (int i = 0; i <= maxIndex && found <= index; i++) { if (data.charAt(i) == separator || i == maxIndex) { found++; strIndex[0] = strIndex[1] + 1; strIndex[1] = (i == maxIndex) ? i + 1 : i; } } return found > index ? data.substring(strIndex[0], strIndex[1]) : ""; } void printDisplayState() { Serial.println("-----------------------------"); Serial.println("[DISPLAY STATE]"); Serial.print(" Jasa : "); Serial.println(strJasa); Serial.print(" Est : "); Serial.println(strEst); Serial.print(" Tipe : "); Serial.println(strTipeLayanan); Serial.print(" Berat : "); Serial.println(strBerat); Serial.print(" Harga : "); Serial.println(strHarga); Serial.println("-----------------------------"); } void reconnectStream() { Serial.println("[STREAM] Menyambung ulang stream Firebase..."); Firebase.RTDB.endStream(&streamDo); delay(200); if (Firebase.RTDB.getString(&fbdo, firebasePath + "/status_hp")) { String rawData = fbdo.stringData(); Serial.print("[STREAM] Data status_hp saat reconnect: "); Serial.println(rawData); strJasa = getValue(rawData, '|', 0); strJasa.trim(); strEst = getValue(rawData, '|', 1); strEst.trim(); strTipeLayanan = getValue(rawData, '|', 2); strTipeLayanan.trim(); strBerat = getValue(rawData, '|', 3); strBerat.trim(); strHarga = getValue(rawData, '|', 4); strHarga.trim(); printDisplayState(); } else { Serial.println("[STREAM] Gagal baca status_hp saat reconnect"); } if (Firebase.RTDB.beginMultiPathStream(&streamDo, firebasePath)) { Serial.println("[STREAM] Stream berhasil disambung ulang"); } else { Serial.println("[STREAM] GAGAL menyambung ulang stream!"); } } void setup() { Serial.begin(9600); Serial.println("\n============================="); Serial.println("[BOOT] Sistem mulai..."); Wire.begin(21, 22); pinMode(BUTTON_PIN, INPUT_PULLUP); pinMode(BUZZER_PIN, OUTPUT); digitalWrite(BUZZER_PIN, LOW); display.begin(I2C_ADDRESS, true); display.setTextWrap(false); display.clearDisplay(); display.setTextColor(SH110X_WHITE); display.setTextSize(1); // CEK RESET WIFI if (digitalRead(BUTTON_PIN) == LOW) { Serial.println("[BOOT] Tombol ditekan saat boot — mode reset WiFi"); display.clearDisplay(); display.setCursor(10, 30); display.print("Tahan 5 detik = Reset"); display.display(); unsigned long pressStart = millis(); while (digitalRead(BUTTON_PIN) == LOW && (millis() - pressStart < 5000)) { delay(10); } if (millis() - pressStart >= 5000) { Serial.println("[BOOT] WiFi direset!"); display.clearDisplay(); display.setCursor(10, 30); display.print("WiFi Direset!"); display.setCursor(10, 45); display.print("Lepas Tombol..."); display.display(); beepBuzzer(3); WiFiManager wm; wm.resetSettings(); delay(1000); while (digitalRead(BUTTON_PIN) == LOW) { delay(10); } delay(500); ESP.restart(); } } // WIFI MANAGER Serial.println("[WIFI] Menghubungkan ke WiFi..."); display.clearDisplay(); display.setCursor(0, 14); display.print("Menghubungkan WiFi"); display.setCursor(0, 28); display.print("Jika gagal,"); display.setCursor(0, 42); display.print("buka: laundry.net"); display.display(); WiFiManager wm; wm.setCaptivePortalEnable(true); wm.setHostname("laundry"); wm.setConnectTimeout(15); wm.setConfigPortalTimeout(60); bool connected = wm.autoConnect("MyLaundry"); if (!connected) { Serial.println("[WIFI] GAGAL terhubung! Restart..."); display.clearDisplay(); display.setCursor(10, 30); display.print("WiFi Gagal!"); display.setCursor(10, 42); display.print("Restart..."); display.display(); delay(3000); ESP.restart(); } Serial.print("[WIFI] Terhubung! IP: "); Serial.println(WiFi.localIP()); // FIREBASE Serial.println("[FIREBASE] Inisialisasi Firebase..."); config.api_key = API_KEY; config.database_url = DATABASE_URL; config.signer.test_mode = true; config.token_status_callback = tokenStatusCallback; fbdo.setBSSLBufferSize(4096, 1024); Firebase.begin(&config, &auth); Firebase.reconnectWiFi(true); Serial.println("[FIREBASE] Firebase siap"); // LOAD CELL Serial.println("[LOADCELL] Inisialisasi load cell..."); scale.begin(LOADCELL_DOUT_PIN, LOADCELL_SCK_PIN); scale.set_scale(calibration_factor); display.clearDisplay(); display.setCursor(10, 35); display.print("Tunggu 5 detik"); display.display(); Serial.println("[LOADCELL] Warm-up 5 detik..."); delay(5000); scale.tare(); Serial.println("[LOADCELL] Tare selesai, siap menimbang"); // CEK STATUS Serial.println("[FIREBASE] Membaca status_hp awal..."); if (Firebase.RTDB.getString(&fbdo, firebasePath + "/status_hp")) { String rawData = fbdo.stringData(); Serial.print("[FIREBASE] status_hp awal: "); Serial.println(rawData); strJasa = getValue(rawData, '|', 0); strJasa.trim(); strEst = getValue(rawData, '|', 1); strEst.trim(); strTipeLayanan = getValue(rawData, '|', 2); strTipeLayanan.trim(); strBerat = getValue(rawData, '|', 3); strBerat.trim(); strHarga = getValue(rawData, '|', 4); strHarga.trim(); printDisplayState(); } else { Serial.println("[FIREBASE] Gagal baca status_hp awal"); } if (Firebase.RTDB.beginMultiPathStream(&streamDo, firebasePath)) { Serial.println("[STREAM] Stream Firebase aktif"); } else { Serial.println("[STREAM] GAGAL memulai stream Firebase!"); } display.clearDisplay(); display.setCursor(10, 35); display.print("Sistem Siap!"); display.display(); beepBuzzer(2); delay(1000); lastStreamCheck = millis(); Serial.println("[BOOT] Setup selesai, masuk loop"); Serial.println("=============================\n"); } void loop() { // CEK STREAM if (millis() - lastStreamCheck > STREAM_CHECK_INTERVAL) { lastStreamCheck = millis(); if (WiFi.status() != WL_CONNECTED) { Serial.println("[WIFI] Koneksi terputus! Mencoba reconnect..."); WiFi.reconnect(); delay(2000); if (WiFi.status() == WL_CONNECTED) { Serial.print("[WIFI] Reconnect berhasil! IP: "); Serial.println(WiFi.localIP()); } else { Serial.println("[WIFI] Reconnect GAGAL"); } } else { Serial.println("[HEALTH] WiFi OK | Stream check..."); } if (!streamDo.httpConnected()) { Serial.println("[STREAM] Stream terputus, reconnect..."); reconnectStream(); } else { Serial.println("[HEALTH] Stream OK"); } } // BACA STREAM FIREBASE if (Firebase.ready() && Firebase.RTDB.readStream(&streamDo)) { if (streamDo.streamAvailable()) { String dPath = streamDo.dataPath(); String dType = streamDo.dataType(); Serial.print("[STREAM] Event masuk — path: "); Serial.print(dPath); Serial.print(" | type: "); Serial.println(dType); if (dPath == "/status_hp" && dType == "string") { String rawData = streamDo.stringData(); Serial.print("[STREAM] status_hp baru: "); Serial.println(rawData); String newJasa = getValue(rawData, '|', 0); newJasa.trim(); if (newJasa != strJasa) { Serial.print("[DISPLAY] Jasa berubah: "); Serial.print(strJasa); Serial.print(" → "); Serial.println(newJasa); strJasa = newJasa; scrollPosX_Jasa = 42; } strEst = getValue(rawData, '|', 1); strEst.trim(); strTipeLayanan = getValue(rawData, '|', 2); strTipeLayanan.trim(); String newBerat = getValue(rawData, '|', 3); newBerat.trim(); if (newBerat != strBerat) { Serial.print("[DISPLAY] Berat berubah: "); Serial.print(strBerat); Serial.print(" → "); Serial.println(newBerat); strBerat = newBerat; scrollPosX_Berat = 42; } strHarga = getValue(rawData, '|', 4); strHarga.trim(); printDisplayState(); } else if (dPath == "/screen_status" && dType == "string") { String screenCmd = streamDo.stringData(); Serial.print("[STREAM] screen_status: "); Serial.println(screenCmd); if (screenCmd == "RESTART") { Serial.println("[CMD] RESTART diterima — reset nilai & reconnect stream"); strJasa = "-"; strEst = "-"; strTipeLayanan = "-"; strBerat = "0.000"; strHarga = "Rp0"; scrollPosX_Jasa = 42; scrollPosX_Berat = 42; Firebase.RTDB.setStringAsync(&fbdo, firebasePath + "/screen_status", "ON"); Serial.println("[CMD] screen_status di-set ke ON"); reconnectStream(); } else { Serial.print("[CMD] screen_status '"); Serial.print(screenCmd); Serial.println("' diterima tapi tidak ada aksi"); } } else if (dPath == "/current_weight" && dType == "float") { Serial.print("[STREAM] current_weight update dari Firebase: "); Serial.println(streamDo.floatData(), 3); } else { Serial.print("[STREAM] Path tidak dihandle: "); Serial.println(dPath); } } } // LOAD CELL & LOGIKA AUTO-LOCK if (scale.is_ready()) { float raw_weight = scale.get_units(2); // Cegah nilai minus if (raw_weight < 0.005f) { raw_weight = 0.00f; } if (isLocked) { // Jika beban diangkat atau ditambah drastis, LEPASKAN KUNCI if (abs(raw_weight - lockedWeight) > TOLERANSI_UNLOCK) { isLocked = false; Serial.println("[LOCK] Kunci terlepas!"); } else { currentWeight = lockedWeight; } } if (!isLocked) { currentWeight = raw_weight; // Cek apakah berat mulai stabil (perubahan di bawah toleransi 20 gram) if (abs(raw_weight - lastWeight) <= TOLERANSI_STABIL) { // Jika sudah stabil selama 2 detik, dan berat di atas 0.1 kg (jangan kunci timbangan kosong) if (millis() - stableStartTime > WAKTU_STABIL && raw_weight > 0.1f) { isLocked = true; lockedWeight = raw_weight; Serial.print("[LOCK] Berat dikunci di: "); Serial.println(lockedWeight, 3); } } else { // Jika berat masih naik-turun, reset timer kestabilan stableStartTime = millis(); lastWeight = raw_weight; } } } // PANTAUAN SERIAL MONITOR if (millis() - lastSerialTime > SERIAL_PRINT_INTERVAL) { lastSerialTime = millis(); Serial.print("Berat : "); Serial.print(currentWeight, 3); Serial.println(" kg"); } // TICK SCROLL bool tickScroll = false; if (millis() - lastScrollTime > 30) { tickScroll = true; lastScrollTime = millis(); } display.clearDisplay(); display.setTextSize(1); //TEXT JASA int textWidthJasa = strJasa.length() * 6; if (textWidthJasa > (128 - 42)) { if (tickScroll) { scrollPosX_Jasa--; if (scrollPosX_Jasa < -textWidthJasa) scrollPosX_Jasa = 128; } display.setCursor(scrollPosX_Jasa, 10); display.print(strJasa); display.fillRect(0, 10, 42, 9, SH110X_BLACK); } else { scrollPosX_Jasa = 42; display.setCursor(42, 10); display.print(strJasa); } display.setCursor(0, 10); display.print("Jasa : "); // ESTIMASI display.setCursor(0, 21); display.print("Est : "); display.print(strEst); // TOTAL RINCIAN String fullBerat = "-"; if (strJasa != "-" && strJasa != "") { if (strBerat != "" && strBerat != "0" && strBerat != "0.0" && strBerat != "0.000") { fullBerat = strBerat; } } int textWidthBerat = fullBerat.length() * 6; if (textWidthBerat > (128 - 42)) { if (tickScroll) { scrollPosX_Berat--; if (scrollPosX_Berat < -textWidthBerat) scrollPosX_Berat = 128; } display.setCursor(scrollPosX_Berat, 32); display.print(fullBerat); display.fillRect(0, 32, 42, 9, SH110X_BLACK); } else { scrollPosX_Berat = 42; display.setCursor(42, 32); display.print(fullBerat); } display.setCursor(0, 32); display.print("Total: "); // HARGA display.setCursor(0, 43); display.print("Harga: "); display.print(strHarga); // GARIS & STATUS BAR display.drawLine(0, 52, 128, 52, SH110X_WHITE); display.setCursor(0, 55); display.print("Berat: "); display.print(currentWeight, 3); display.print(" Kg"); if (currentWeight >= 20.0f) { display.print(" OVL!"); } // TOMBOL KIRIM BERAT if (digitalRead(BUTTON_PIN) == LOW) { delay(50); if (digitalRead(BUTTON_PIN) == LOW) { Serial.print("[BUTTON] Tombol ditekan — kirim berat: "); Serial.print(currentWeight, 3); Serial.println(" kg"); display.fillRect(0, 54, 128, 10, SH110X_BLACK); display.setCursor(0, 55); display.print("Mengirim..."); display.display(); bool berhasil = false; if (Firebase.ready()) { unsigned long startSend = millis(); if (Firebase.RTDB.setFloat(&fbdo, firebasePath + "/current_weight", currentWeight)) { if (millis() - startSend < 5000) { berhasil = true; } } } if (berhasil) { Serial.println("[BUTTON] Kirim berat BERHASIL"); beepBuzzer(2); } else { Serial.println("[BUTTON] Kirim berat GAGAL — reconnect stream"); display.fillRect(0, 54, 128, 10, SH110X_BLACK); display.setCursor(0, 55); display.print("GAGAL! Coba lagi"); display.display(); beepBuzzer(1); delay(1500); reconnectStream(); } unsigned long startWait = millis(); while (digitalRead(BUTTON_PIN) == LOW && (millis() - startWait < 2000)) { delay(10); } } } display.display(); delay(10); }