1179 lines
47 KiB
C++
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
|