544 lines
16 KiB
C++
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);
|
|
}
|