TKK_E32231213/edasmart_esp32/edasmart_esp32.ino

1179 lines
47 KiB
C++

// ============================================================
// EdaSmart ESP32 Firmware
// ============================================================
#include <Wire.h>
#include <PCF8574.h>
#include <LiquidCrystal_I2C.h>
#include <WiFi.h>
#include <Preferences.h>
#include <WebServer.h>
#include <Update.h>
#include <PubSubClient.h>
#include <WiFiClientSecure.h>
// ==================== 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"(<!DOCTYPE html><html><head><title>EdaSmart Update</title>
<meta name='viewport' content='width=device-width'>
<style>body{font-family:Arial;text-align:center;padding:20px;}h2{color:#2e7d32;}
input[type='submit']{background:#2e7d32;color:white;padding:10px 30px;border:none;border-radius:5px;cursor:pointer;font-size:16px;}</style>
</head><body><h2>EdaSmart Firmware Update</h2><p>Pilih file .bin lalu klik Upload</p>
<form method='POST' action='/update' enctype='multipart/form-data'>
<input type='file' name='firmware' accept='.bin'><br><br>
<input type='submit' value='Upload Firmware'></form></body></html>)";
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) menuOffset--; renderMenu(); } }
void menuNavDown() { if(menuIndex<MENU_COUNT-1){ 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 && mesinIndex<MESIN_COUNT-1){ mesinIndex++; renderKontrolMesin(); }
if (okPressed) {
mesinAktif = mesinIndex;
if (mesinAktif==0) { mesinTimer.menit=1; currentState=MESIN_SET_WAKTU; renderSetWaktu(); }
else { currentState=MESIN_SET_KECEPATAN; renderSetKecepatan(); }
}
if (backPressed) { currentState=MAIN_MENU; renderMenu(); }
break;
case SETTING_MENU:
if (upPressed && settingIndex>0) { settingIndex--; renderSettingMenu(); }
if (downPressed && settingIndex<SETTING_COUNT-1){ settingIndex++; renderSettingMenu(); }
if (okPressed) {
if (settingIndex==0) { currentState=WIFI_MENU; wifiIndex=0; renderWiFiMenu(); }
else { currentState=OTA_MENU; renderOTAMenu(); }
}
if (backPressed) { currentState=MAIN_MENU; renderMenu(); }
break;
case WIFI_MENU:
if (!wifiStatusShowing) {
if (upPressed && wifiIndex>0) { wifiIndex--; renderWiFiMenu(); }
if (downPressed && wifiIndex<WIFI_COUNT-1){ wifiIndex++; renderWiFiMenu(); }
if (okPressed) {
if (wifiIndex==0) { mulaiScan(); }
else {
wifiStatusShowing=true; lcd.clear(); printCenter(0,"-- Status Koneksi --");
lcd.setCursor(0,1);
if (WiFi.status()==WL_CONNECTED) { lcd.print("WiFi: "); lcd.print(WiFi.localIP()); }
else lcd.print("WiFi: Tdk Terhubung");
lcd.setCursor(0,2); lcd.print("MQTT: "); lcd.print(mqttClient.connected()?"Terhubung":"Tdk Terhubung");
lcd.setCursor(0,3); lcd.print("BACK = Kembali");
}
}
if (backPressed) { currentState=SETTING_MENU; renderSettingMenu(); }
} else {
if (backPressed||okPressed) { wifiStatusShowing=false; renderWiFiMenu(); }
}
break;
case WIFI_SCANNING: {
if (millis()-scanAnimLast>=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) scanOffset--; renderScanMenu(); }
if (downPressed && scanIndex<scanCount-1) { 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()<MAX_WIFI_PASSWORD) { inputPassword+=charSet[charIndex]; renderPassword(); }
else { lcd.setCursor(0,3); lcd.print("Password max 64 char"); }
}
okHolding=false;
}
if (backPressed) {
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 && kecepatan<PWM_MAX) { kecepatan+=PWM_STEP; if(kecepatan>PWM_MAX)kecepatan=PWM_MAX; renderSetKecepatan(); }
if (downPressed && kecepatan>PWM_MIN){ kecepatan-=PWM_STEP; if(kecepatan<PWM_MIN)kecepatan=PWM_MIN; renderSetKecepatan(); }
if (okPressed) {
simpanKecepatan();
// Publish kecepatan terbaru ke backend supaya app bisa sync
if (mesinAktif == 1) mqttPublishGiling(rpmNilai, 0);
mesinTimer.menit=1; currentState=MESIN_SET_WAKTU; renderSetWaktu();
}
if (backPressed) { currentState=KONTROL_MESIN_MENU; renderKontrolMesin(); }
break;
}
case MESIN_COUNTDOWN:
handleBuzzer();
if (mesinTimer.running) {
if (millis()-timerTerakhir>=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