TKK_E32230801/mylaundry.ino

544 lines
16 KiB
C++

#include "HX711.h"
#include <Adafruit_GFX.h>
#include <Adafruit_SH110X.h>
#include <Firebase_ESP_Client.h>
#include <Preferences.h>
#include <WiFi.h>
#include <WiFiManager.h>
#include <Wire.h>
#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);
}