// ============================================================ // EdaSmart ESP32 Firmware // ============================================================ #include #include #include #include #include #include #include #include #include // ==================== MQTT HiveMQ Cloud ==================== #define MQTT_SERVER "8dd0d24eadfb4eb0b1e6fd87628f0f4e.s1.eu.hivemq.cloud" #define MQTT_PORT 8883 #define MQTT_USER "Edasmart" #define MQTT_PASS "KentangBalado1" // ==================== TOPICS: Publish (ESP32 → Backend) ==================== // Payload JSON: {"status":"ON","phase":"TAHAN","sisa":45} #define TOPIC_PRESS "edasmart/press" // Payload JSON: {"status":"ON","rpm":1200,"sisa":30} #define TOPIC_GILING "edasmart/giling" // Payload JSON: {"aktif":true} #define TOPIC_ESTOP "edasmart/estop" // Payload JSON: {"status":"ONLINE"} #define TOPIC_DEVICE_STATUS "edasmart/device" // Payload JSON: {"jarak":15.3,"status":"normal"} #define TOPIC_SENSOR "edasmart/sensor" // ==================== TOPICS: Subscribe (Backend → ESP32) ==================== // Payload JSON: {"mesin":"press","perintah":"STOP"} #define TOPIC_CMD "edasmart/cmd" // ==================== PIN DEFINITIONS ==================== #define SDA_PIN 21 #define SCL_PIN 22 #define BTN_OK 1 // PCF8574 P1 #define BTN_BACK 0 // PCF8574 P0 #define BTN_UP 3 // PCF8574 P3 #define BTN_DOWN 2 // PCF8574 P2 #define BTN_ESTOP 6 // PCF8574 P6, NC #define BTN_LS_ATAS 4 // PCF8574 P4, NC #define BTN_LS_BAWAH 5 // PCF8574 P5, NC #define BUZZER_PIN 23 #define ENA 32 #define ENB 33 #define RELAY_TAMBAHAN 25 // pin bekas IN1, sekarang relay tambahan // IN2 (pin 26) tidak digunakan #define IN3 27 #define IN4 14 #define RELAY_PRESS 17 #define RELAY_PENGGILING 16 #define LED_BUILTIN_PIN 2 #define HALL_PIN 34 #define TRIG_PIN 18 #define ECHO_PIN 19 // ==================== CONSTANTS ==================== #define MODE_SIMULASI false // true = satuan waktu detik, false = menit #define LCD_WIDTH 20 #define LCD_HEIGHT 4 #define MAX_WIFI_PASSWORD 64 #define WIFI_CONNECT_TIMEOUT 15000 #define LOOP_INTERVAL 20 #define LEDC_FREQ 1000 #define LEDC_RES 8 #define PWM_STEP 5 #define PWM_MIN 5 #define PWM_MAX 100 #define PRESS_SPD_TURUN 180 #define PRESS_SPD_NAIK 153 #define PRESS_SPD_MUNDUR 102 #define HALL_MAGNET_COUNT 1 #define SONAR_THRESHOLD_CM 10 #define SONAR_TIMEOUT_US 30000 #define BUZZER_INTERVAL 200 #define TIMER_UPDATE_INTERVAL 1000 // ==================== MQTT ==================== WiFiClientSecure espClient; PubSubClient mqttClient(espClient); bool mqttConnected = false; unsigned long mqttLastReconnect = 0; // ==================== GLOBAL OBJECTS ==================== Preferences prefs; PCF8574 pcf(0x20); LiquidCrystal_I2C lcd(0x27, LCD_WIDTH, LCD_HEIGHT); // ==================== STATE MACHINE ==================== enum State { INTRO, MAIN_MENU, KONTROL_MESIN_MENU, SETTING_MENU, WIFI_MENU, WIFI_SCANNING, WIFI_SCAN_MENU, WIFI_PASSWORD_MENU, WIFI_CONNECTING, OTA_MENU, MESIN_SET_WAKTU, MESIN_SET_KECEPATAN, MESIN_COUNTDOWN, PRESS_TURUN, PRESS_TAHAN, PRESS_NAIK, PRESS_MUNDUR, PRESS_SONAR_WARNING }; // ==================== STRUCTS ==================== struct Button { bool last = false; bool pressed(bool now) { bool triggered = now && !last; last = now; return triggered; } }; struct MesinTimer { int menit = 1; int detik = 0; unsigned long startTime = 0; bool running = false; bool done = false; }; // ==================== GLOBAL VARIABLES ==================== State currentState = INTRO; bool wifiStatusShowing = false; bool wifiGagalShowing = false; unsigned long wifiGagalStart = 0; bool otaOffShowing = false; unsigned long otaOffStart = 0; bool scanGagalShowing = false; unsigned long scanGagalStart = 0; bool eStopAktif = false; bool eStopTampil = false; bool lsAtasAktif = false; bool lsBawahAktif = false; bool pressBackMode = false; bool otaRestartPending = false; unsigned long otaRestartStart = 0; bool sonarWarningAktif = false; // Hall Effect RPM volatile unsigned long hallPulseCount = 0; unsigned long rpmLast = 0; unsigned long rpmNilai = 0; unsigned long rpmInterval = 1000; // LED unsigned long ledLast = 0; bool ledState = false; // Button instances Button btnOK, btnBack, btnUp, btnDown; // Menu states int menuIndex = 0, menuOffset = 0; int settingIndex = 0, mesinIndex = 0, wifiIndex = 0; int scanIndex = 0, scanOffset = 0, scanCount = 0; // WiFi input String selectedSSID = ""; String inputPassword = ""; int charIndex = 0; const String charSet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 !@#$"; unsigned long scanAnimLast = 0; int scanAnimIndex = 0; // Mesin MesinTimer mesinTimer; int mesinAktif = 0; // 0=Press, 1=Penggiling // OTA bool otaAktif = false; WebServer otaServer(80); // UI flags bool introDone = false; bool sudahTampilConnect = false; unsigned long connectStart = 0; unsigned long lastDot = 0; int dotCount = 0; bool aboutShowing = false; unsigned long aboutStart = 0; unsigned long introLast = 0; int introIndex = 0; bool introSetupDone = false; String introText = "EdaSmart"; // Buzzer bool buzzerDone = false; int buzzerCount = 0; unsigned long buzzerLast = 0; unsigned long timerTerakhir = 0; bool finalBuzzerAktif = false; int finalBuzzerStep = 0; unsigned long finalBuzzerLast = 0; bool selesaiTampil = false; unsigned long selesaiStart = 0; // Sonar unsigned long sonarLast = 0; #define SONAR_PUBLISH_INTERVAL 1000 // kirim data sonar ke backend tiap 1 detik // Kecepatan (dalam persen 0-100) int kecepatanPress = 100; int kecepatanPenggiling = 100; // ==================== CUSTOM CHARACTERS ==================== byte daunKiri[8] = {B00011,B00111,B01111,B11110,B11100,B01100,B00100,B00000}; byte daunKanan[8] = {B11000,B11100,B11110,B01111,B00111,B00110,B00100,B00000}; byte arrowRight[8]= {B00000,B01000,B01100,B01110,B01100,B01000,B00000,B00000}; byte arrowUp[8] = {B00100,B01110,B11111,B00100,B00100,B00100,B00000,B00000}; byte arrowDown[8] = {B00100,B00100,B00100,B11111,B01110,B00100,B00000,B00000}; byte wifiKiri[8] = {B00000,B00001,B00011,B00111,B00011,B00001,B00001,B00000}; byte wifiKanan[8] = {B00000,B10000,B11000,B11100,B11000,B10000,B10000,B00000}; byte wifiOK[8] = {B00000,B00001,B00011,B10110,B11100,B01000,B00000,B00000}; byte wifiNo[8] = {B00000,B10001,B01010,B00100,B01010,B10001,B00000,B00000}; #define CHR_DAUN_KIRI 0 #define CHR_DAUN_KANAN 1 #define CHR_ARROW 2 #define CHR_UP 3 #define CHR_DOWN 4 #define CHR_WIFI_L 5 #define CHR_WIFI_R 6 #define CHR_WIFI_OK 7 // ==================== MENU DATA ==================== const char* menuItems[] = {"Kontrol Mesin", "Setting", "About"}; const char* settingItems[] = {"WiFi", "Update Firmware"}; const char* mesinItems[] = {"Mesin Press", "Mesin Penggiling"}; const char* wifiItems[] = {"Scan WiFi", "Status Koneksi"}; const int MENU_COUNT = 3; const int SETTING_COUNT = 2; const int MESIN_COUNT = 2; const int WIFI_COUNT = 2; // ==================== FORWARD DECLARATIONS ==================== void mqttPublishEStop(bool aktif); void mqttPublishIdle(); void mqttPublishPress(const String& phase, int sisaDetik); void mqttPublishGiling(int rpm, int sisaDetik); void mqttPublishSensor(float jarak); void renderKontrolMesin(); void renderMenu(); void tampilStatusWifi(); void renderPressStatus(const String& status); void renderCountdown(); float bacaJarak(); void renderSonarWarning(float jarak); // ==================== UTILITY ==================== void printCenter(int row, const String& text) { int pad = (LCD_WIDTH - text.length()) / 2; lcd.setCursor(pad, row); lcd.print(text); } void printLine(int row, char c = '-') { lcd.setCursor(0, row); for (int i = 0; i < LCD_WIDTH; i++) lcd.print(c); } // ==================== MOTOR CONTROL ==================== // RELAY_TAMBAHAN (pin 25, bekas IN1) dipakai sebagai relay tambahan untuk mesin press. // IN2 (pin 26) tidak digunakan lagi — arah motor tidak dikontrol via H-bridge. // ENA tetap dipakai untuk PWM kecepatan mesin press. void mesinPressNyala() { digitalWrite(RELAY_PRESS, LOW); digitalWrite(RELAY_TAMBAHAN, HIGH); ledcWrite(ENA, persenKePwm(kecepatanPress)); } void mesinPressMati() { ledcWrite(ENA, 0); digitalWrite(RELAY_TAMBAHAN, LOW); digitalWrite(RELAY_PRESS, HIGH); } void mesinPenggilingNyala() { digitalWrite(RELAY_PENGGILING, LOW); digitalWrite(IN3,HIGH); digitalWrite(IN4,LOW); ledcWrite(ENB, persenKePwm(kecepatanPenggiling)); } void mesinPenggilingMati() { digitalWrite(IN3,LOW); digitalWrite(IN4,LOW); ledcWrite(ENB, 0); digitalWrite(RELAY_PENGGILING, HIGH); hallPulseCount=0; rpmNilai=0; } void nyalakanMesin() { if (mesinAktif==0) mesinPressNyala(); else mesinPenggilingNyala(); } void matikanMesin() { if (mesinAktif==0) mesinPressMati(); else mesinPenggilingMati(); } void matikanSemuaMesin() { mesinPressMati(); mesinPenggilingMati(); } void pressGerakTurun() { digitalWrite(RELAY_PRESS,LOW); digitalWrite(RELAY_TAMBAHAN, HIGH); ledcWrite(ENA, PRESS_SPD_TURUN); } void pressGerakNaik() { digitalWrite(RELAY_PRESS,LOW); digitalWrite(RELAY_TAMBAHAN, HIGH); ledcWrite(ENA, PRESS_SPD_NAIK); } void pressGerakMundur() { digitalWrite(RELAY_PRESS,LOW); digitalWrite(RELAY_TAMBAHAN, HIGH); ledcWrite(ENA, PRESS_SPD_MUNDUR); } void pressBerhenti() { ledcWrite(ENA,0); digitalWrite(RELAY_TAMBAHAN, LOW); digitalWrite(RELAY_PRESS,HIGH); } int pwmKePersen(int pwm) { return map(pwm, 0, 255, 0, 100); } int persenKePwm(int persen) { return map(persen, 0, 100, 0, 255); } void setupLEDC() { ledcAttach(ENA, LEDC_FREQ, LEDC_RES); ledcAttach(ENB, LEDC_FREQ, LEDC_RES); } // ==================== BUZZER ==================== void handleBuzzer() { if (buzzerDone) return; unsigned long now = millis(); if (now - buzzerLast >= BUZZER_INTERVAL) { buzzerLast = now; digitalWrite(BUZZER_PIN, (buzzerCount % 2 == 0) ? HIGH : LOW); buzzerCount++; if (buzzerCount >= 4) { digitalWrite(BUZZER_PIN, LOW); buzzerDone = true; mesinTimer.startTime = millis(); mesinTimer.running = true; if (mesinAktif != 0) nyalakanMesin(); } } } void handleFinalBuzzer() { if (!finalBuzzerAktif) return; unsigned long now = millis(); switch (finalBuzzerStep) { case 0: digitalWrite(BUZZER_PIN,HIGH); finalBuzzerLast=now; finalBuzzerStep++; break; case 1: if(now-finalBuzzerLast>=1000){ digitalWrite(BUZZER_PIN,LOW); finalBuzzerLast=now; finalBuzzerStep++; } break; case 2: if(now-finalBuzzerLast>=200) { digitalWrite(BUZZER_PIN,HIGH); finalBuzzerLast=now; finalBuzzerStep++; } break; case 3: if(now-finalBuzzerLast>=200) { digitalWrite(BUZZER_PIN,LOW); finalBuzzerLast=now; finalBuzzerStep++; } break; case 4: if(now-finalBuzzerLast>=200) { digitalWrite(BUZZER_PIN,HIGH); finalBuzzerLast=now; finalBuzzerStep++; } break; case 5: if(now-finalBuzzerLast>=200) { digitalWrite(BUZZER_PIN,LOW); finalBuzzerAktif=false; finalBuzzerStep=0; selesaiTampil=true; selesaiStart=millis(); } break; } } // ==================== LED ==================== void handleLED() { unsigned long now = millis(); if (eStopAktif) { if(now-ledLast>=50) { ledLast=now; ledState=!ledState; digitalWrite(LED_BUILTIN_PIN,ledState); } return; } if (otaAktif) { if(now-ledLast>=100) { ledLast=now; ledState=!ledState; digitalWrite(LED_BUILTIN_PIN,ledState); } return; } if (mesinTimer.running) { if(now-ledLast>=500) { ledLast=now; ledState=!ledState; digitalWrite(LED_BUILTIN_PIN,ledState); } return; } if (WiFi.status()==WL_CONNECTED) { digitalWrite(LED_BUILTIN_PIN,HIGH); ledState=true; return; } digitalWrite(LED_BUILTIN_PIN,LOW); ledState=false; } // ==================== RPM ==================== void IRAM_ATTR hallISR() { hallPulseCount++; } void hitungRPM() { unsigned long now = millis(); if (now - rpmLast >= rpmInterval) { rpmNilai = (hallPulseCount * 60UL) / HALL_MAGNET_COUNT; hallPulseCount = 0; rpmLast = now; } } // ==================== SONAR ==================== float bacaJarak() { // Coba baca sampai 3x, ambil hasil valid pertama for (int i = 0; i < 3; i++) { digitalWrite(TRIG_PIN, LOW); delayMicroseconds(2); digitalWrite(TRIG_PIN, HIGH); delayMicroseconds(10); digitalWrite(TRIG_PIN, LOW); long durasi = pulseIn(ECHO_PIN, HIGH, SONAR_TIMEOUT_US); if (durasi > 0) return durasi / 58.0; delay(10); // tunggu sebentar sebelum retry } return 999.0; // semua retry gagal } // Publish data sonar secara periodik ke backend (tiap SONAR_PUBLISH_INTERVAL ms) // Hanya aktif saat state PRESS_TURUN (sebelum press mulai bergerak) void handleSonar() { if (currentState != PRESS_TURUN) return; if (!mqttClient.connected()) return; unsigned long now = millis(); if (now - sonarLast < SONAR_PUBLISH_INTERVAL) return; sonarLast = now; float jarak = bacaJarak(); if (jarak < 999.0) mqttPublishSensor(jarak); } // ==================== MQTT FUNCTIONS ==================== void mqttCallback(char* topic, byte* payload, unsigned int length) { String msg = ""; for (int i = 0; i < length; i++) msg += (char)payload[i]; // Topic: edasmart/cmd // Payload JSON: {"mesin":"press","perintah":"STOP"} atau {"mesin":"giling","perintah":"START"} if (String(topic) == TOPIC_CMD) { // Tentukan mesin yang dituju bool untukPress = msg.indexOf("\"mesin\":\"press\"") >= 0; bool untukGiling = msg.indexOf("\"mesin\":\"giling\"") >= 0; // ── STOP ────────────────────────────────────────────── if (msg.indexOf("\"perintah\":\"STOP\"") >= 0) { if ((untukPress && mesinAktif == 0) || (untukGiling && mesinAktif == 1) || (!untukPress && !untukGiling)) { matikanMesin(); mesinTimer.running = false; mqttPublishIdle(); currentState = KONTROL_MESIN_MENU; renderKontrolMesin(); } } // ── START ───────────────────────────────────────────── // Jalankan mesin dengan setting terakhir (kecepatan & waktu dari Preferences) else if (msg.indexOf("\"perintah\":\"START\"") >= 0) { if (eStopAktif) return; // abaikan kalau e-stop aktif if (untukPress) mesinAktif = 0; if (untukGiling) mesinAktif = 1; // Baca durasi dari payload JSON jika ada: {"mesin":"giling","perintah":"START","durasi":5} int durasiDariApp = 0; int idxDurasi = msg.indexOf("\"durasi\":"); if (idxDurasi >= 0) { durasiDariApp = msg.substring(idxDurasi + 9).toInt(); } // Baca kecepatan dari payload JSON jika ada: {"mesin":"giling","perintah":"START","kecepatan":191} // Nilai kecepatan sudah dalam satuan PWM (0-255), dikonversi dari % oleh backend int idxKecepatan = msg.indexOf("\"kecepatan\":"); if (idxKecepatan >= 0) { // Backend kirim langsung dalam persen (0-100), clamp dan bulatkan ke kelipatan 5 int persenDariApp = msg.substring(idxKecepatan + 12).toInt(); persenDariApp = constrain(persenDariApp, PWM_MIN, PWM_MAX); persenDariApp = ((persenDariApp + 2) / 5) * 5; persenDariApp = constrain(persenDariApp, PWM_MIN, PWM_MAX); if (untukGiling || mesinAktif == 1) { kecepatanPenggiling = persenDariApp; simpanKecepatan(); } if (untukPress || mesinAktif == 0) { kecepatanPress = persenDariApp; simpanKecepatan(); } } // Reset timer & mulai buzzer countdown sebelum nyala // Jika durasi = 0 (tidak dikirim dari app/LCD), mesin nyala tanpa batas waktu (9999 menit) mesinTimer.menit = (durasiDariApp > 0) ? durasiDariApp : 9999; mesinTimer.startTime = 0; mesinTimer.running = false; mesinTimer.done = false; buzzerDone = false; buzzerCount = 0; buzzerLast = millis(); // Untuk press, langsung masuk fase TURUN if (mesinAktif == 0) { currentState = PRESS_TURUN; pressGerakTurun(); mqttPublishPress("TURUN", mesinTimer.menit); renderPressStatus("TURUN"); } else { // Penggiling: masuk MESIN_COUNTDOWN, buzzer berbunyi dulu // nyalakanMesin() dan startTime akan diset oleh handleBuzzer() setelah buzzer selesai currentState = MESIN_COUNTDOWN; renderCountdown(); } } } } void mqttReconnect() { if (WiFi.status() != WL_CONNECTED) return; if (millis() - mqttLastReconnect < 5000) return; mqttLastReconnect = millis(); Serial.println("[MQTT] Mencoba konek..."); if (mqttClient.connect("EdaSmart", MQTT_USER, MQTT_PASS)) { Serial.println("[MQTT] Berhasil konek!"); mqttConnected = true; mqttClient.subscribe(TOPIC_CMD); mqttClient.publish(TOPIC_DEVICE_STATUS, "{\"status\":\"ONLINE\"}", true); } else { Serial.print("[MQTT] Gagal, rc="); Serial.println(mqttClient.state()); mqttConnected = false; } } void mqttPublishPress(const String& phase, int sisaDetik) { if (!mqttClient.connected()) return; // Payload JSON: {"status":"ON","phase":"TAHAN","sisa":45} String payload = "{\"status\":\"ON\",\"phase\":\"" + phase + "\",\"sisa\":" + String(sisaDetik) + "}"; mqttClient.publish(TOPIC_PRESS, payload.c_str()); } void mqttPublishGiling(int rpm, int sisaDetik) { if (!mqttClient.connected()) return; // Payload JSON: {"status":"ON","rpm":1200,"sisa":30,"kecepatan":75} String payload = "{\"status\":\"ON\",\"rpm\":" + String(rpm) + ",\"sisa\":" + String(sisaDetik) + ",\"kecepatan\":" + String(kecepatanPenggiling) + "}"; mqttClient.publish(TOPIC_GILING, payload.c_str()); } void mqttPublishIdle() { if (!mqttClient.connected()) return; if (mesinAktif == 0) { mqttClient.publish(TOPIC_PRESS, "{\"status\":\"OFF\",\"phase\":\"IDLE\",\"sisa\":0}"); } else { mqttClient.publish(TOPIC_GILING, "{\"status\":\"OFF\",\"rpm\":0,\"sisa\":0}"); } } void mqttPublishEStop(bool aktif) { if (!mqttClient.connected()) return; String payload = aktif ? "{\"aktif\":true}" : "{\"aktif\":false}"; mqttClient.publish(TOPIC_ESTOP, payload.c_str(), true); } void mqttPublishSensor(float jarak) { if (!mqttClient.connected()) return; // Tentukan status berdasarkan jarak String status; if (jarak < 5.0) status = "bahaya"; else if (jarak < 10.0) status = "peringatan"; else status = "normal"; // Payload JSON: {"jarak":15.3,"status":"normal"} String payload = "{\"jarak\":" + String(jarak, 1) + ",\"status\":\"" + status + "\"}"; mqttClient.publish(TOPIC_SENSOR, payload.c_str()); } // ==================== UI FUNCTIONS ==================== void tampilStatusWifi() { if (WiFi.status() == WL_CONNECTED) { lcd.createChar(7, wifiOK); lcd.setCursor(18, 0); lcd.write(byte(CHR_WIFI_L)); lcd.write(byte(7)); } else { lcd.createChar(7, wifiNo); lcd.setCursor(18, 0); lcd.write(byte(CHR_WIFI_L)); lcd.write(byte(7)); } } void tampilIntro() { if (introDone) return; if (!introSetupDone) { lcd.clear(); printLine(0); lcd.setCursor(3,1); lcd.write(byte(CHR_DAUN_KIRI)); lcd.write(byte(CHR_DAUN_KANAN)); lcd.setCursor(15,1); lcd.write(byte(CHR_DAUN_KIRI)); lcd.write(byte(CHR_DAUN_KANAN)); printCenter(2, "Smart IoT System"); printLine(3); introSetupDone = true; } if (millis() - introLast >= 110) { introLast = millis(); int startCol = (LCD_WIDTH - introText.length()) / 2; lcd.setCursor(startCol + introIndex, 1); lcd.print(introText[introIndex]); introIndex++; if (introIndex >= introText.length()) introDone = true; } } void resetIntro() { introDone=false; introSetupDone=false; introIndex=0; introLast=0; lcd.clear(); } void renderMenu() { lcd.clear(); lcd.setCursor(2,0); lcd.write(byte(CHR_DAUN_KIRI)); lcd.write(byte(CHR_DAUN_KANAN)); lcd.print(" MAIN MENU "); lcd.write(byte(CHR_DAUN_KIRI)); lcd.write(byte(CHR_DAUN_KANAN)); for (int i = 0; i < 3; i++) { int idx = menuOffset + i; if (idx >= MENU_COUNT) break; lcd.setCursor(0, i+1); if (idx == menuIndex) lcd.write(byte(CHR_ARROW)); else lcd.print(" "); lcd.print(" "); lcd.print(idx+1); lcd.print(". "); lcd.print(menuItems[idx]); } if (menuOffset > 0) { lcd.setCursor(LCD_WIDTH-1, 1); lcd.write(byte(CHR_UP)); } if (menuOffset + 3 < MENU_COUNT){ lcd.setCursor(LCD_WIDTH-1, 3); lcd.write(byte(CHR_DOWN)); } tampilStatusWifi(); } void renderKontrolMesin() { lcd.clear(); lcd.setCursor(1,0); lcd.write(byte(CHR_DAUN_KIRI)); lcd.print(" KONTROL MESIN "); lcd.write(byte(CHR_DAUN_KANAN)); for (int i = 0; i < MESIN_COUNT; i++) { lcd.setCursor(1, i+1); if (i == mesinIndex) lcd.write(byte(CHR_ARROW)); else lcd.print(" "); lcd.print(" "); lcd.print(mesinItems[i]); } tampilStatusWifi(); } void renderSettingMenu() { lcd.clear(); lcd.setCursor(4,0); lcd.write(byte(CHR_DAUN_KIRI)); lcd.write(byte(CHR_DAUN_KANAN)); lcd.print(" SETTING "); lcd.write(byte(CHR_DAUN_KIRI)); lcd.write(byte(CHR_DAUN_KANAN)); for (int i = 0; i < SETTING_COUNT; i++) { lcd.setCursor(1, i+1); if (i == settingIndex) lcd.write(byte(CHR_ARROW)); else lcd.print(" "); lcd.print(" "); lcd.print(settingItems[i]); if (i == 1 && otaAktif) lcd.print(" [ON]"); } tampilStatusWifi(); } void renderWiFiMenu() { lcd.clear(); lcd.setCursor(3,0); lcd.write(byte(CHR_DAUN_KIRI)); lcd.write(byte(CHR_DAUN_KANAN)); lcd.print(" WIFI "); lcd.write(byte(CHR_DAUN_KIRI)); lcd.write(byte(CHR_DAUN_KANAN)); for (int i = 0; i < WIFI_COUNT; i++) { lcd.setCursor(1, i+1); if (i == wifiIndex) lcd.write(byte(CHR_ARROW)); else lcd.print(" "); lcd.print(" "); lcd.print(wifiItems[i]); } } void renderScanMenu() { lcd.clear(); lcd.setCursor(0,0); lcd.print("-- Pilih WiFi --"); if (scanCount == 0) { printCenter(2, "Tidak ada jaringan"); return; } for (int i = 0; i < 3; i++) { int idx = scanOffset + i; if (idx >= scanCount) break; lcd.setCursor(0, i+1); if (idx == scanIndex) lcd.write(byte(CHR_ARROW)); else lcd.print(" "); lcd.print(" "); String ssid = WiFi.SSID(idx); if (ssid.length() > 17) ssid = ssid.substring(0, 17); lcd.print(ssid); } } void renderPassword() { lcd.setCursor(0,1); lcd.print(" "); lcd.setCursor(0,1); String tampil = inputPassword; if (tampil.length() > 19) tampil = tampil.substring(tampil.length()-19); lcd.print(tampil); lcd.setCursor(0,2); lcd.print("Char: ["); lcd.print(charSet[charIndex]); lcd.print("] "); } void renderSetWaktu() { lcd.clear(); lcd.setCursor(0,0); lcd.print(mesinAktif==0 ? "-- Mesin Press --" : "-- M.Penggiling--"); printCenter(1, "Set Waktu:"); lcd.setCursor(7,2); lcd.print(mesinTimer.menit); lcd.print(MODE_SIMULASI ? " Detik" : " Menit"); lcd.setCursor(0,3); lcd.print("UP/DN=Ubah OK=Start"); } void renderSetKecepatan() { lcd.clear(); lcd.setCursor(0,0); lcd.print(mesinAktif==0 ? "-- Mesin Press --" : "-- M.Penggiling--"); printCenter(1, "Set Kecepatan:"); int persen = (mesinAktif==0) ? kecepatanPress : kecepatanPenggiling; lcd.setCursor(7,2); lcd.print(persen); lcd.print(" % "); lcd.setCursor(0,3); lcd.print("UP/DN=Ubah OK=Lanjut"); } void renderCountdown() { lcd.clear(); lcd.setCursor(0,0); lcd.print(mesinAktif==0 ? "-- Mesin Press --" : "-- M.Penggiling--"); lcd.setCursor(4,1); lcd.print("Sisa Waktu:"); lcd.setCursor(6,2); if (mesinTimer.menit >= 9999) { // Tidak ada batas waktu — tampilkan --:-- lcd.print("--:--"); } else { long elapsed = (long)((millis() - mesinTimer.startTime) / 1000); int totalDetik = MODE_SIMULASI ? (int)mesinTimer.menit - (int)elapsed : (mesinTimer.menit*60) - (int)elapsed; if (totalDetik < 0) totalDetik = 0; int mnt = totalDetik/60, dtk = totalDetik%60; if(mnt<10) lcd.print("0"); lcd.print(mnt); lcd.print(":"); if(dtk<10) lcd.print("0"); lcd.print(dtk); } if (mesinAktif == 1) { lcd.setCursor(0,3); lcd.print("RPM:"); lcd.print(rpmNilai); lcd.print(" "); } else { lcd.setCursor(0,3); lcd.print("BACK=Stop"); } } void renderPressStatus(const String& status) { lcd.clear(); lcd.setCursor(0,0); lcd.print("-- Mesin Press --"); printCenter(1, status); if (currentState == PRESS_TAHAN) { long elapsed = (long)((millis() - mesinTimer.startTime) / 1000); int totalDetik = MODE_SIMULASI ? (int)mesinTimer.menit - (int)elapsed : (mesinTimer.menit*60) - (int)elapsed; if (totalDetik < 0) totalDetik = 0; int mnt=totalDetik/60, dtk=totalDetik%60; lcd.setCursor(6,2); if(mnt<10) lcd.print("0"); lcd.print(mnt); lcd.print(":"); if(dtk<10) lcd.print("0"); lcd.print(dtk); } lcd.setCursor(0,3); lcd.print("BACK=Stop+Naik"); } void renderSonarWarning(float jarak) { lcd.clear(); printCenter(0, "!! TIDAK ADA BENDA"); lcd.setCursor(0,1); lcd.print("Jarak: "); lcd.print(jarak,1); lcd.print(" cm"); printCenter(2, "Letakkan benda dulu"); printCenter(3, "OK = Coba lagi"); } void renderOTAMenu() { lcd.clear(); printCenter(0, "Update Firmware"); lcd.setCursor(0,1); lcd.print("Status: "); lcd.print(otaAktif ? "ON " : "OFF"); lcd.setCursor(0,3); lcd.print(otaAktif ? "OK=OFF BACK=Keluar" : "OK=ON BACK=Keluar"); } // ==================== WIFI & PREFS ==================== void loadWifi() { prefs.begin("wifi", true); selectedSSID = prefs.getString("ssid", ""); inputPassword = prefs.getString("pass", ""); prefs.end(); } void simpanWifi() { prefs.begin("wifi", false); prefs.putString("ssid", selectedSSID); prefs.putString("pass", inputPassword); prefs.end(); } void loadKecepatan() { prefs.begin("mesin", true); kecepatanPress = prefs.getInt("spd_press", 100); kecepatanPenggiling = prefs.getInt("spd_giling", 100); prefs.end(); } void simpanKecepatan() { prefs.begin("mesin", false); prefs.putInt("spd_press", kecepatanPress); prefs.putInt("spd_giling", kecepatanPenggiling); prefs.end(); } void mulaiScan() { lcd.clear(); printCenter(0, "-- Scan WiFi --"); printCenter(2, "Mohon tunggu"); WiFi.mode(WIFI_STA); WiFi.disconnect(); WiFi.scanNetworks(true); scanAnimIndex=0; scanAnimLast=millis(); scanIndex=0; scanOffset=0; currentState = WIFI_SCANNING; } void initPassword() { inputPassword=""; charIndex=0; lcd.clear(); lcd.setCursor(0,0); lcd.print("Password:"); lcd.setCursor(0,3); lcd.print("Masukkan Password"); renderPassword(); } void mulaiConnect() { simpanWifi(); connectStart=millis(); sudahTampilConnect=false; lastDot=0; dotCount=0; lcd.clear(); printCenter(0,"Menghubungkan..."); String ssidTampil = selectedSSID; if (ssidTampil.length()>14) ssidTampil=ssidTampil.substring(0,14); lcd.setCursor(0,1); lcd.print("SSID: "); lcd.print(ssidTampil); printCenter(3,"Mohon tunggu"); WiFi.begin(selectedSSID.c_str(), inputPassword.c_str()); } // ==================== OTA ==================== const char* otaHtmlPage = R"(EdaSmart Update

EdaSmart Firmware Update

Pilih file .bin lalu klik Upload



)"; void nyalakanOTA() { WiFi.mode(WIFI_AP); WiFi.softAP("EdaSmart-Update", "edasmart2026"); otaServer.on("/", HTTP_GET, []() { if (!otaServer.authenticate("admin", "edasmart2026")) return otaServer.requestAuthentication(); otaServer.send(200, "text/html", otaHtmlPage); }); otaServer.on("/update", HTTP_POST, []() { bool ok = !Update.hasError(); otaServer.send(200, "text/plain", ok ? "Berhasil! Restart..." : "GAGAL! Coba lagi."); if (ok) { otaRestartPending=true; otaRestartStart=millis(); } }, []() { HTTPUpload& upload = otaServer.upload(); if (upload.status == UPLOAD_FILE_START) { lcd.clear(); printCenter(0,"Uploading..."); Update.begin(UPDATE_SIZE_UNKNOWN); } else if (upload.status == UPLOAD_FILE_WRITE) { Update.write(upload.buf, upload.currentSize); lcd.setCursor(0,2); lcd.print("Size: "); lcd.print(upload.totalSize/1024); lcd.print(" KB "); } else if (upload.status == UPLOAD_FILE_END) { if (Update.end(true)) { lcd.clear(); printCenter(1,"Update Selesai!"); printCenter(2,"Restart..."); } else { lcd.clear(); printCenter(1,"Update GAGAL!"); } } } ); otaServer.begin(); otaAktif = true; lcd.clear(); printCenter(0,"OTA Aktif"); lcd.setCursor(0,1); lcd.print("WiFi:EdaSmart-Update"); lcd.setCursor(0,2); lcd.print("IP: 192.168.4.1"); printCenter(3,"BACK = Matikan"); } void matikanOTA() { otaServer.stop(); WiFi.softAPdisconnect(true); if (selectedSSID != "") { WiFi.mode(WIFI_STA); WiFi.begin(selectedSSID.c_str(), inputPassword.c_str()); } otaAktif = false; lcd.clear(); printCenter(1,"OTA Dimatikan"); } // ==================== E-STOP ==================== void handleEStop(uint8_t pcfVal) { bool eStopNow = bitRead(pcfVal, BTN_ESTOP) == HIGH; if (eStopNow && !eStopAktif) { eStopAktif = true; mqttPublishEStop(true); matikanSemuaMesin(); digitalWrite(BUZZER_PIN, LOW); finalBuzzerAktif=false; finalBuzzerStep=0; mesinTimer.running = false; lcd.clear(); printCenter(0,"!!! EMERGENCY !!!"); printCenter(1,"!!! STOP !!!"); printCenter(2,"Semua mesin mati"); printCenter(3,"Lepas tombol utk OK"); digitalWrite(LED_BUILTIN_PIN, HIGH); eStopTampil = true; } if (!eStopNow && eStopAktif) { eStopAktif = false; mqttPublishEStop(false); lcd.clear(); printCenter(1,"E-Stop dilepas"); printCenter(2,"Tekan OK lanjut"); } } // ==================== NAVIGATION ==================== void menuNavUp() { if(menuIndex>0){ menuIndex--; if(menuIndex=menuOffset+3) menuOffset++; renderMenu(); } } // ==================== SETUP ==================== void setup() { Serial.begin(115200); Wire.begin(SDA_PIN, SCL_PIN); pcf.begin(); lcd.init(); lcd.backlight(); lcd.createChar(CHR_DAUN_KIRI, daunKiri); lcd.createChar(CHR_DAUN_KANAN, daunKanan); lcd.createChar(CHR_ARROW, arrowRight); lcd.createChar(CHR_UP, arrowUp); lcd.createChar(CHR_DOWN, arrowDown); lcd.createChar(CHR_WIFI_L, wifiKiri); lcd.createChar(CHR_WIFI_R, wifiKanan); lcd.createChar(CHR_WIFI_OK, wifiOK); pinMode(HALL_PIN, INPUT); attachInterrupt(digitalPinToInterrupt(HALL_PIN), hallISR, FALLING); pinMode(ENA, OUTPUT); pinMode(ENB, OUTPUT); pinMode(RELAY_TAMBAHAN, OUTPUT); // relay tambahan, pin bekas IN1 pinMode(IN3, OUTPUT); pinMode(IN4, OUTPUT); pinMode(RELAY_PRESS, OUTPUT); pinMode(RELAY_PENGGILING, OUTPUT); pinMode(BUZZER_PIN, OUTPUT); pinMode(LED_BUILTIN_PIN, OUTPUT); pinMode(TRIG_PIN, OUTPUT); pinMode(ECHO_PIN, INPUT); digitalWrite(TRIG_PIN, LOW); digitalWrite(LED_BUILTIN_PIN, LOW); digitalWrite(RELAY_TAMBAHAN, LOW); // relay tambahan default OFF digitalWrite(RELAY_PRESS, HIGH); digitalWrite(RELAY_PENGGILING, HIGH); digitalWrite(BUZZER_PIN, LOW); setupLEDC(); matikanSemuaMesin(); loadWifi(); loadKecepatan(); espClient.setInsecure(); mqttClient.setServer(MQTT_SERVER, MQTT_PORT); mqttClient.setCallback(mqttCallback); if (selectedSSID != "") { Serial.println("[WiFi] Auto connect ke: " + selectedSSID); WiFi.begin(selectedSSID.c_str(), inputPassword.c_str()); int timeout = 0; while (WiFi.status() != WL_CONNECTED && timeout < 20) { delay(500); timeout++; } if (WiFi.status() == WL_CONNECTED) Serial.println("[WiFi] Terhubung otomatis!"); } } unsigned long lastLoopTime = 0; // ==================== MAIN LOOP ==================== void loop() { if (otaRestartPending && millis() - otaRestartStart >= 1000) ESP.restart(); if (otaAktif) { otaServer.handleClient(); return; } handleFinalBuzzer(); handleLED(); hitungRPM(); handleSonar(); if (WiFi.status() == WL_CONNECTED) { if (!mqttClient.connected()) { mqttConnected=false; mqttReconnect(); } else { mqttConnected=true; mqttClient.loop(); } } unsigned long now = millis(); if (now - lastLoopTime < LOOP_INTERVAL) return; lastLoopTime = now; uint8_t pcfVal = pcf.read8(); handleEStop(pcfVal); if (eStopAktif) { btnOK.pressed(bitRead(pcfVal,BTN_OK)==LOW); btnBack.pressed(bitRead(pcfVal,BTN_BACK)==LOW); btnUp.pressed(bitRead(pcfVal,BTN_UP)==LOW); btnDown.pressed(bitRead(pcfVal,BTN_DOWN)==LOW); return; } if (!eStopAktif && eStopTampil) { if (btnOK.pressed(bitRead(pcfVal,BTN_OK)==LOW)) { eStopTampil=false; currentState=MAIN_MENU; menuIndex=0; menuOffset=0; renderMenu(); } return; } bool okNow = bitRead(pcfVal,BTN_OK)==LOW; bool backNow = bitRead(pcfVal,BTN_BACK)==LOW; bool upNow = bitRead(pcfVal,BTN_UP)==LOW; bool downNow = bitRead(pcfVal,BTN_DOWN)==LOW; bool okPressed = btnOK.pressed(okNow); bool backPressed = btnBack.pressed(backNow); bool upPressed = btnUp.pressed(upNow); bool downPressed = btnDown.pressed(downNow); if (aboutShowing && millis()-aboutStart>=2000) { aboutShowing=false; renderMenu(); } if (selesaiTampil && millis()-selesaiStart>=1500) { selesaiTampil=false; currentState=KONTROL_MESIN_MENU; renderKontrolMesin(); } switch (currentState) { case INTRO: tampilIntro(); if (okPressed) { currentState=MAIN_MENU; renderMenu(); } break; case MAIN_MENU: if (upPressed) menuNavUp(); if (downPressed) menuNavDown(); if (okPressed) { switch (menuIndex) { case 0: currentState=KONTROL_MESIN_MENU; mesinIndex=0; renderKontrolMesin(); break; case 1: currentState=SETTING_MENU; settingIndex=0; renderSettingMenu(); break; case 2: lcd.clear(); printCenter(0,"-- About --"); printCenter(1,"EdaSmart v1.0"); printCenter(2,"Smart IoT System"); printCenter(3,"edasmart.com"); aboutShowing=true; aboutStart=millis(); break; } } if (backPressed) { currentState=INTRO; resetIntro(); } break; case KONTROL_MESIN_MENU: if (upPressed && mesinIndex>0) { mesinIndex--; renderKontrolMesin(); } if (downPressed && mesinIndex0) { settingIndex--; renderSettingMenu(); } if (downPressed && settingIndex0) { wifiIndex--; renderWiFiMenu(); } if (downPressed && wifiIndex=400) { scanAnimLast=millis(); lcd.setCursor(6,1); switch(scanAnimIndex%4){ case 0:lcd.print(". ");break; case 1:lcd.print(".. ");break; case 2:lcd.print("... ");break; case 3:lcd.print(" ");break; } scanAnimIndex++; } if (backPressed) { WiFi.scanDelete(); scanGagalShowing=false; currentState=WIFI_MENU; renderWiFiMenu(); break; } if (scanGagalShowing) { if (millis()-scanGagalStart>=1500) { scanGagalShowing=false; currentState=WIFI_MENU; renderWiFiMenu(); } break; } int result = WiFi.scanComplete(); if (result==WIFI_SCAN_RUNNING) break; if (result==WIFI_SCAN_FAILED) { scanGagalShowing=true; scanGagalStart=millis(); lcd.clear(); printCenter(1,"Scan Gagal!"); printCenter(2,"Coba lagi"); break; } scanCount=result; currentState=WIFI_SCAN_MENU; renderScanMenu(); break; } case WIFI_SCAN_MENU: if (upPressed && scanIndex>0) { scanIndex--; if(scanIndex=scanOffset+3) scanOffset++; renderScanMenu(); } if (okPressed) { selectedSSID=WiFi.SSID(scanIndex); currentState=WIFI_PASSWORD_MENU; initPassword(); } if (backPressed) { currentState=WIFI_MENU; renderWiFiMenu(); } break; case WIFI_PASSWORD_MENU: { static unsigned long okHoldStart = 0; static bool okHolding = false; if (upPressed) { charIndex=(charIndex-1+charSet.length())%charSet.length(); renderPassword(); } if (downPressed) { charIndex=(charIndex+1)%charSet.length(); renderPassword(); } if (okNow) { if (!okHolding) { okHolding=true; okHoldStart=millis(); } else if (millis()-okHoldStart>=2000) { okHolding=false; currentState=WIFI_CONNECTING; mulaiConnect(); } } else { if (okHolding && millis()-okHoldStart<2000) { if (inputPassword.length()0) { inputPassword.remove(inputPassword.length()-1); renderPassword(); } else { currentState=WIFI_SCAN_MENU; renderScanMenu(); } } break; } case WIFI_CONNECTING: if (wifiGagalShowing) { if (millis()-wifiGagalStart>=2000) { wifiGagalShowing=false; currentState=WIFI_PASSWORD_MENU; initPassword(); } break; } if (WiFi.status()==WL_CONNECTED) { if (!sudahTampilConnect) { sudahTampilConnect=true; lcd.clear(); printCenter(0,"Terhubung!"); lcd.setCursor(0,1); lcd.print("IP: "); lcd.print(WiFi.localIP()); printCenter(3,"OK = Kembali"); } if (okPressed) { currentState=WIFI_MENU; renderWiFiMenu(); } } else if (millis()-connectStart>WIFI_CONNECT_TIMEOUT) { if (!wifiGagalShowing) { wifiGagalShowing=true; wifiGagalStart=millis(); lcd.clear(); printCenter(1,"Gagal Terhubung!"); printCenter(2,"Cek password"); } } else { if (millis()-lastDot>500) { lastDot=millis(); lcd.setCursor(dotCount+7,2); lcd.print("."); dotCount=(dotCount+1)%6; } } if (backPressed) { wifiGagalShowing=false; WiFi.disconnect(); currentState=WIFI_MENU; renderWiFiMenu(); } break; case OTA_MENU: if (otaOffShowing) { if(millis()-otaOffStart>=1500){ otaOffShowing=false; currentState=SETTING_MENU; renderSettingMenu(); } break; } if (okPressed) { if (!otaAktif) nyalakanOTA(); else { matikanOTA(); otaOffShowing=true; otaOffStart=millis(); } } if (backPressed) { if (otaAktif) { matikanOTA(); otaOffShowing=true; otaOffStart=millis(); } else { currentState=SETTING_MENU; renderSettingMenu(); } } break; case MESIN_SET_WAKTU: if (upPressed && mesinTimer.menit<60) { mesinTimer.menit++; renderSetWaktu(); } if (downPressed && mesinTimer.menit>1) { mesinTimer.menit--; renderSetWaktu(); } if (okPressed) { buzzerDone=false; buzzerCount=0; buzzerLast=millis(); timerTerakhir=0; mesinTimer.startTime=0; mesinTimer.running=false; mesinTimer.done=false; lcd.clear(); lcd.setCursor(0,0); lcd.print(mesinAktif==0?"-- Mesin Press --":"-- M.Penggiling--"); printCenter(2,"Bersiap..."); if (mesinAktif==0) { currentState=PRESS_TURUN; pressBackMode=false; } else { currentState=MESIN_COUNTDOWN; } } if (backPressed) { currentState=KONTROL_MESIN_MENU; renderKontrolMesin(); } break; case MESIN_SET_KECEPATAN: { int& kecepatan = (mesinAktif==0) ? kecepatanPress : kecepatanPenggiling; if (upPressed && kecepatanPWM_MAX)kecepatan=PWM_MAX; renderSetKecepatan(); } if (downPressed && kecepatan>PWM_MIN){ kecepatan-=PWM_STEP; if(kecepatan=TIMER_UPDATE_INTERVAL) { timerTerakhir=millis(); renderCountdown(); long el=(long)((millis()-mesinTimer.startTime)/1000); int sisa = 0; if (mesinTimer.menit < 9999) { sisa = MODE_SIMULASI ? (int)mesinTimer.menit - (int)el : (mesinTimer.menit*60) - (int)el; if(sisa<0) sisa=0; } mqttPublishGiling(rpmNilai, sisa); } long elapsed=(long)((millis()-mesinTimer.startTime)/1000); if (mesinTimer.menit < 9999) { int totalDetik=MODE_SIMULASI ? (int)mesinTimer.menit - (int)elapsed : (mesinTimer.menit*60) - (int)elapsed; if (totalDetik<=0) { mesinTimer.running=false; matikanMesin(); mqttPublishIdle(); currentState=KONTROL_MESIN_MENU; lcd.clear(); printCenter(1,"Selesai!"); printCenter(2,"Mesin Berhenti"); finalBuzzerAktif=true; } } } if (backPressed) { mesinTimer.running=false; matikanMesin(); mqttPublishIdle(); finalBuzzerAktif=false; finalBuzzerStep=0; selesaiTampil=false; digitalWrite(BUZZER_PIN,LOW); currentState=KONTROL_MESIN_MENU; renderKontrolMesin(); } break; case PRESS_TURUN: { handleBuzzer(); if (!buzzerDone) break; static bool sonarSudahCek = false; if (!sonarSudahCek) { sonarSudahCek = true; sonarLast = 0; // reset timer supaya handleSonar langsung publish di iterasi pertama float jarak = bacaJarak(); if (jarak >= SONAR_THRESHOLD_CM) { digitalWrite(BUZZER_PIN,HIGH); delay(300); digitalWrite(BUZZER_PIN,LOW); renderSonarWarning(jarak); sonarWarningAktif=true; currentState=PRESS_SONAR_WARNING; break; } digitalWrite(RELAY_PRESS, LOW); } lsBawahAktif = bitRead(pcfVal, BTN_LS_BAWAH)==HIGH; pressGerakTurun(); if (millis()-timerTerakhir>=500) { timerTerakhir=millis(); renderPressStatus("Turun..."); mqttPublishPress("TURUN",0); } if (lsBawahAktif) { pressBerhenti(); mesinTimer.startTime=millis(); mesinTimer.running=true; timerTerakhir=0; sonarSudahCek=false; currentState=PRESS_TAHAN; renderPressStatus("Menahan..."); } if (backPressed) { sonarSudahCek=false; pressBackMode=true; currentState=PRESS_NAIK; pressGerakNaik(); } break; } case PRESS_TAHAN: { if (millis()-timerTerakhir>=TIMER_UPDATE_INTERVAL) { timerTerakhir=millis(); renderPressStatus("Menahan..."); long el=(long)((millis()-mesinTimer.startTime)/1000); int sisa=MODE_SIMULASI ? (int)mesinTimer.menit - (int)el : (mesinTimer.menit*60) - (int)el; if(sisa<0)sisa=0; mqttPublishPress("TAHAN",sisa); } long elapsed=(long)((millis()-mesinTimer.startTime)/1000); int totalDetik=MODE_SIMULASI ? (int)mesinTimer.menit - (int)elapsed : (mesinTimer.menit*60) - (int)elapsed; if (totalDetik<=0) { mesinTimer.running=false; currentState=PRESS_NAIK; pressGerakNaik(); timerTerakhir=0; renderPressStatus("Naik..."); } if (backPressed) { pressBackMode=true; currentState=PRESS_NAIK; pressGerakNaik(); } break; } case PRESS_NAIK: { lsAtasAktif = bitRead(pcfVal, BTN_LS_ATAS)==HIGH; pressGerakNaik(); if (millis()-timerTerakhir>=500) { timerTerakhir=millis(); renderPressStatus("Naik..."); mqttPublishPress("NAIK",0); } if (lsAtasAktif) { timerTerakhir=0; currentState=PRESS_MUNDUR; renderPressStatus("Turun sedikit..."); } break; } case PRESS_MUNDUR: { lsAtasAktif = bitRead(pcfVal, BTN_LS_ATAS)==HIGH; if (lsAtasAktif) { pressGerakTurun(); if (millis()-timerTerakhir>=500) { timerTerakhir=millis(); mqttPublishPress("MUNDUR",0); } } else { pressBerhenti(); mqttPublishIdle(); if (pressBackMode) { pressBackMode=false; currentState=KONTROL_MESIN_MENU; renderKontrolMesin(); } else { // Pindah state ke KONTROL_MESIN_MENU supaya motor tidak bisa di-trigger ulang, // tapi tampilkan layar selesai dulu via finalBuzzer → selesaiTampil → renderKontrolMesin currentState=KONTROL_MESIN_MENU; lcd.clear(); printCenter(1,"Selesai!"); printCenter(2,"Mesin Berhenti"); finalBuzzerAktif=true; finalBuzzerStep=0; } } break; } case PRESS_SONAR_WARNING: { if (okPressed) { float jarak = bacaJarak(); if (jarak < SONAR_THRESHOLD_CM) { sonarWarningAktif=false; buzzerDone=false; buzzerCount=0; buzzerLast=millis(); timerTerakhir=0; lcd.clear(); lcd.setCursor(0,0); lcd.print("-- Mesin Press --"); printCenter(2,"Bersiap..."); currentState=PRESS_TURUN; } else { renderSonarWarning(jarak); digitalWrite(BUZZER_PIN,HIGH); delay(100); digitalWrite(BUZZER_PIN,LOW); } } if (backPressed) { sonarWarningAktif=false; pressBerhenti(); currentState=KONTROL_MESIN_MENU; renderKontrolMesin(); } break; } } // end switch } // end loop