#include <SPI.h>
#include <Adafruit_GFX.h>
#include <Adafruit_ILI9341.h>
// ===== PIN CONFIGURATION =====
#define TFT_CS 15
#define TFT_DC 2
#define TFT_RST 5
#define ENC_A 32
#define ENC_B 33
#define ENC_SW 4
#define HALL_SENSOR_PIN 26
// ===== TFT INSTANCE =====
Adafruit_ILI9341 tft = Adafruit_ILI9341(TFT_CS, TFT_DC, TFT_RST);
// ===== CONSTANTS =====
const int MAX_PRESETS = 25;
const int NAME_LEN = 4;
const int MAX_STEPS = 5;
const unsigned long DEBOUNCE_DELAY = 200;
const unsigned long ENCODER_DELAY = 100; // Slow down encoder
// ===== COLORS =====
#define COLOR_BACKGROUND ILI9341_BLACK
#define COLOR_HEADER ILI9341_BLUE
#define COLOR_MENU_TEXT ILI9341_YELLOW
#define COLOR_MENU_HIGHLIGHT ILI9341_MAGENTA
#define COLOR_MENU_SELECTED ILI9341_WHITE
#define COLOR_FIELD_SELECTED ILI9341_PINK
#define COLOR_FIELD_EDIT ILI9341_BLUE
// ===== ENUMS =====
enum Page {
PAGE_MAIN_MENU,
PAGE_MANUAL,
PAGE_PROGRAM,
PAGE_LOG,
PAGE_SETTING
};
enum ExecutionContext {
CONTEXT_MANUAL_DIRECT,
CONTEXT_PROGRAM_START
};
enum RotationMode {
ROT_NORMAL = 0,
ROT_REVERSE,
ROT_NORMAL_PULSE,
ROT_REVERSE_PULSE
};
enum SystemStatus {
STATUS_STOPPED,
STATUS_RUNNING,
STATUS_PAUSED,
STATUS_COOLING
};
enum ManualField {
FIELD_VOLTAGE = 0,
FIELD_ROTATION,
FIELD_LOG, // Tambahkan field Log
FIELD_PLAY_PAUSE,
FIELD_NEXT,
FIELD_STOP,
FIELD_COUNT
};
// ===== DATA STRUCTURES =====
struct PresetData {
bool isUsed;
char name[NAME_LEN + 1];
int cycleCount;
unsigned long rStop;
int targetRPM;
float voltages[MAX_STEPS];
uint8_t directions[MAX_STEPS];
int durations[MAX_STEPS];
int coolingTimes[MAX_STEPS];
};
struct SystemState {
Page currentPage;
int selectedMenuItem;
ManualField selectedField;
bool editMode;
ExecutionContext context;
int activePresetIndex;
int currentCycle;
int totalCycles;
int currentStep;
float currentVoltage;
RotationMode rotationMode;
SystemStatus status;
int currentRPM;
int maxRPM;
int temperature;
unsigned long startTime;
unsigned long runningTime;
unsigned long coolingTime;
bool timerActive;
bool isCooling;
unsigned long lastTimerUpdate;
unsigned long lastRPMUpdate;
};
// ===== GLOBAL VARIABLES =====
PresetData presets[MAX_PRESETS];
SystemState sys;
// Encoder variables - FIXED
int lastEncoderA = HIGH;
bool lastSWState = HIGH;
unsigned long lastDebounce = 0;
unsigned long lastEncoderTime = 0; // For slowing down encoder
// Hall sensor
volatile int pulseCount = 0;
// Menu items
const int MENU_COUNT = 4;
const char* menuItems[MENU_COUNT] = {"Manual", "Program", "Log", "Setting"};
const char* rotationNames[] = {"Normal", "Reverse", "N-Pulse", "R-Pulse"};
// ===== INTERRUPT HANDLERS =====
void IRAM_ATTR countPulse() {
pulseCount++;
}
// ===== ENCODER FUNCTIONS - FIXED =====
int readEncoder() {
// Slow down encoder response
if (millis() - lastEncoderTime < ENCODER_DELAY) {
return 0;
}
int currentA = digitalRead(ENC_A);
int currentB = digitalRead(ENC_B);
static int lastA = HIGH;
int delta = 0;
if (lastA == HIGH && currentA == LOW) {
if (currentB == HIGH) {
delta = 1; // Clockwise
} else {
delta = -1; // Counter-clockwise
}
lastEncoderTime = millis();
}
lastA = currentA;
return delta;
}
bool readSWClick() {
bool sw = digitalRead(ENC_SW);
if (sw == LOW && lastSWState == HIGH && millis() - lastDebounce > DEBOUNCE_DELAY) {
lastDebounce = millis();
lastSWState = LOW;
return true;
}
lastSWState = sw;
return false;
}
// ===== UTILITY FUNCTIONS =====
String formatTime(unsigned long timeInMillis) {
unsigned long minutes = timeInMillis / 60000;
unsigned long seconds = (timeInMillis % 60000) / 1000;
return String(minutes) + "'" + (seconds < 10 ? "0" : "") + String(seconds);
}
// ===== PRESET MANAGEMENT =====
void initializePresets() {
for (int i = 0; i < MAX_PRESETS; i++) {
presets[i].isUsed = false;
strcpy(presets[i].name, " ");
presets[i].cycleCount = 1;
presets[i].rStop = 0;
presets[i].targetRPM = 0;
for (int step = 0; step < MAX_STEPS; step++) {
presets[i].voltages[step] = 0.0;
presets[i].directions[step] = 0;
presets[i].durations[step] = 0;
presets[i].coolingTimes[step] = 0;
}
}
}
void loadManualDefaults() {
sys.activePresetIndex = -1;
sys.context = CONTEXT_MANUAL_DIRECT;
sys.currentCycle = 1;
sys.totalCycles = 99;
sys.currentStep = 1;
sys.currentVoltage = 0.0;
sys.rotationMode = ROT_NORMAL;
sys.status = STATUS_STOPPED;
}
// ===== TIMER FUNCTIONS =====
void startTimers(unsigned long userRunningTime, unsigned long userCoolingTime) {
sys.runningTime = userRunningTime * 60 * 1000;
sys.coolingTime = userCoolingTime * 60 * 1000;
sys.startTime = millis();
sys.timerActive = true;
sys.isCooling = false;
sys.status = STATUS_RUNNING;
}
void updateTimers() {
unsigned long currentTime = millis();
if (currentTime - sys.lastTimerUpdate < 1000) return;
sys.lastTimerUpdate = currentTime;
if (sys.timerActive && sys.status == STATUS_RUNNING) {
if (!sys.isCooling) {
unsigned long elapsed = currentTime - sys.startTime;
if (elapsed >= sys.runningTime) {
sys.isCooling = true;
sys.startTime = currentTime;
sys.status = STATUS_COOLING;
}
} else {
unsigned long elapsed = currentTime - sys.startTime;
if (elapsed >= sys.coolingTime) {
sys.timerActive = false;
sys.status = STATUS_STOPPED;
}
}
}
}
void updateRPM() {
if (millis() - sys.lastRPMUpdate >= 1000) {
noInterrupts();
int pulses = pulseCount;
pulseCount = 0;
interrupts();
sys.currentRPM = pulses * 60;
if (sys.currentRPM > sys.maxRPM) sys.maxRPM = sys.currentRPM;
sys.lastRPMUpdate = millis();
}
}
// ===== NAVIGATION FUNCTIONS =====
void handleMenuNavigation(int encoderDelta) {
if (encoderDelta != 0) {
// Pastikan hanya satu arah yang diizinkan
if (encoderDelta > 0) {
sys.selectedMenuItem++;
} else {
sys.selectedMenuItem--;
}
if (sys.selectedMenuItem < 0) sys.selectedMenuItem = MENU_COUNT - 1;
if (sys.selectedMenuItem >= MENU_COUNT) sys.selectedMenuItem = 0;
}
}
void handleManualNavigation(int encoderDelta) {
if (!sys.editMode && encoderDelta != 0) {
int newField = (int)sys.selectedField + encoderDelta;
if (newField < 0) newField = FIELD_COUNT - 1;
if (newField >= FIELD_COUNT) newField = 0;
sys.selectedField = (ManualField)newField;
} else if (sys.editMode && encoderDelta != 0) {
switch (sys.selectedField) {
case FIELD_VOLTAGE:
sys.currentVoltage += encoderDelta * 0.1;
if (sys.currentVoltage < 0.0) sys.currentVoltage = 0.0;
if (sys.currentVoltage > 7.0) sys.currentVoltage = 7.0;
sys.currentVoltage = round(sys.currentVoltage * 10.0) / 10.0;
break;
case FIELD_ROTATION:
sys.rotationMode = (RotationMode)((sys.rotationMode + (encoderDelta > 0 ? 1 : -1) + 4) % 4);
break;
}
}
}
// ===== DRAWING FUNCTIONS =====
void drawMainMenu() {
tft.fillScreen(COLOR_BACKGROUND);
tft.setTextColor(COLOR_HEADER);
tft.setTextSize(2);
tft.setCursor(90, 10);
tft.print("Motor Break In");
for (int i = 0; i < MENU_COUNT; i++) {
int y = 40 + i * 25;
if (i == sys.selectedMenuItem) {
tft.fillRect(10, y - 2, 220, 22, COLOR_MENU_HIGHLIGHT);
tft.setTextColor(COLOR_MENU_SELECTED);
} else {
tft.setTextColor(COLOR_MENU_TEXT);
}
tft.setCursor(20, y);
tft.setTextSize(2);
tft.print(menuItems[i]);
}
// Removed WiFi status
// tft.setTextColor(COLOR_MENU_TEXT);
// tft.setTextSize(2);
// tft.setCursor(20, 150);
// tft.print("WiFi");
tft.setTextSize(1);
tft.setTextColor(COLOR_MENU_SELECTED);
tft.setCursor(10, 220);
tft.print("ver 1.0.0");
tft.setTextColor(COLOR_HEADER);
tft.setCursor(170, 220);
tft.print("Kanslay Hobbies Garage");
}
void drawPresetInfo() {
// Clear the top line first
tft.fillRect(0, 0, 110, 20, COLOR_BACKGROUND);
tft.fillRect(160, 0, 160, 20, COLOR_BACKGROUND);
tft.setTextSize(1);
tft.setTextColor(ILI9341_BLUE);
// Preset info
tft.setCursor(10, 5);
if (sys.context == CONTEXT_MANUAL_DIRECT) {
tft.print("0 : MANU");
} else {
String presetLabel = String(presets[sys.activePresetIndex].name).substring(0, 4);
tft.print(String(sys.activePresetIndex + 1) + " : " + presetLabel);
}
// Hapus bagian ini untuk menghilangkan "Cycle 1/99"
// tft.setCursor(110, 5);
// tft.print(String(sys.currentCycle) + "/" + String(sys.totalCycles));
// Step info
tft.setCursor(197, 5);
if (sys.context == CONTEXT_MANUAL_DIRECT) {
tft.print("Step: 1/1");
} else {
tft.print("Step: " + String(sys.currentStep) + "/" + String(MAX_STEPS));
}
// Temperature
tft.setCursor(280, 5);
tft.print(sys.temperature);
tft.print((char)176);
tft.print("C");
}
void drawRPMDisplay() {
// FIXED: Clear RPM area properly
tft.fillRect(20, 25, 260, 25, COLOR_BACKGROUND);
tft.setTextSize(3);
tft.setTextColor(ILI9341_WHITE);
// Current RPM
tft.setCursor(25, 30);
tft.print(sys.currentRPM);
// Separator
tft.setCursor(140, 30);
tft.print("/");
// Max RPM
tft.setCursor(170, 30);
tft.print(sys.maxRPM);
// RPM label - FIXED position
tft.setTextSize(1);
tft.setCursor(285, 43);
tft.print("rpm");
}
void drawStatusDisplay() {
const char* statusText;
uint16_t statusColor;
switch (sys.status) {
case STATUS_RUNNING:
statusText = "Running";
statusColor = ILI9341_GREEN;
break;
case STATUS_PAUSED:
statusText = "Paused";
statusColor = ILI9341_YELLOW;
break;
case STATUS_COOLING:
statusText = "Cooling";
statusColor = ILI9341_CYAN;
break;
default:
statusText = "Stopped";
statusColor = ILI9341_RED;
break;
}
tft.fillRect(25, 59, 160, 20, statusColor);
tft.setCursor(67, 59);
tft.setTextColor(ILI9341_WHITE);
tft.setTextSize(2);
tft.print(statusText);
}
void drawCycleField() {
// Hanya tampilkan informasi cycle tanpa background selection
tft.setTextSize(1);
tft.setCursor(110, 5);
tft.setTextColor(ILI9341_CYAN);
tft.print(String(sys.currentCycle) + "/" + String(sys.totalCycles));
}
void drawVoltageBox() {
// FIXED: Clear voltage area properly
tft.fillRect(260, 105, 45, 25, COLOR_BACKGROUND);
tft.setTextSize(2);
// Show selection state
uint16_t bgColor = COLOR_BACKGROUND;
uint16_t textColor = ILI9341_WHITE;
if (sys.selectedField == FIELD_VOLTAGE) {
if (sys.editMode) {
bgColor = COLOR_FIELD_EDIT;
textColor = ILI9341_WHITE;
} else {
bgColor = COLOR_FIELD_SELECTED;
textColor = ILI9341_WHITE;
}
}
tft.fillRect(261, 107, 38, 20, bgColor);
tft.setCursor(263, 110);
tft.setTextColor(textColor);
tft.print(String(sys.currentVoltage, 1));
}
void drawRotationMode() {
// FIXED: Clear rotation area properly
tft.fillRect(220, 163, 100, 25, COLOR_BACKGROUND);
tft.setTextSize(2);
// Show selection state
uint16_t bgColor = COLOR_BACKGROUND;
uint16_t textColor = ILI9341_WHITE;
if (sys.selectedField == FIELD_ROTATION) {
if (sys.editMode) {
bgColor = COLOR_FIELD_EDIT;
textColor = ILI9341_WHITE;
} else {
bgColor = COLOR_FIELD_SELECTED;
textColor = ILI9341_WHITE;
}
}
tft.fillRect(220, 165, 95, 20, bgColor);
tft.setCursor(222, 167);
tft.setTextColor(textColor);
tft.print(rotationNames[sys.rotationMode]);
}
void drawTargetRPM() {
// FIXED: Clear target RPM area
tft.fillRect(250, 75, 70, 15, COLOR_BACKGROUND);
tft.setCursor(255, 80);
tft.setTextColor(ILI9341_WHITE);
tft.setTextSize(1);
if (sys.context == CONTEXT_MANUAL_DIRECT) {
tft.print("0");
} else {
int targetRPM = presets[sys.activePresetIndex].isUsed ?
presets[sys.activePresetIndex].targetRPM : 0;
tft.print(targetRPM);
}
tft.setCursor(272, 80);
tft.print("rpm");
}
void drawGraphBox() {
int graphX = 10;
int graphY = 100;
int graphWidth = 185;
int graphHeight = 100;
// Clear graph area
tft.fillRect(graphX, graphY, graphWidth + 50, graphHeight, COLOR_BACKGROUND);
// Draw graph lines
tft.drawLine(graphX, graphY + graphHeight, graphX + graphWidth, graphY + graphHeight, ILI9341_WHITE);
tft.drawLine(graphX + graphWidth, graphY, graphX + graphWidth, graphY + graphHeight, ILI9341_WHITE);
// Labels
tft.setTextSize(1);
tft.setCursor(200, 95);
tft.setTextColor(ILI9341_WHITE);
tft.print("kv");
tft.setCursor(285, 95);
tft.print("0");
tft.setTextSize(2);
tft.setCursor(200, 110);
tft.print("0.0");
tft.setCursor(248, 110);
tft.print("/");
tft.setTextSize(1);
tft.setTextColor(ILI9341_ORANGE);
tft.setCursor(305, 118);
tft.print("v");
// Ampere display
float ampere = 0.0;
tft.setCursor(250, 140);
tft.setTextColor(ILI9341_WHITE);
tft.setTextSize(2);
tft.print(String(ampere, 2));
tft.setCursor(305, 150);
tft.setTextColor(ILI9341_WHITE);
tft.setTextSize(1);
tft.print("A");
}
void drawLogField() {
// Clear log area
tft.fillRect(240, 185, 50, 20, COLOR_BACKGROUND);
// Show selection state
uint16_t bgColor = COLOR_BACKGROUND;
uint16_t textColor = ILI9341_WHITE;
if (sys.selectedField == FIELD_LOG) {
if (sys.editMode) {
bgColor = COLOR_FIELD_EDIT;
textColor = ILI9341_WHITE;
} else {
bgColor = COLOR_FIELD_SELECTED;
textColor = ILI9341_BLACK;
}
}
// Draw log field
tft.fillRect(240, 185, 50, 20, bgColor);
tft.setCursor(242, 187);
tft.setTextColor(textColor);
tft.setTextSize(2);
tft.print("Log");
}
void drawTimers() {
// FIXED: Clear timer area properly
tft.fillRect(10, 208, 190, 32, COLOR_BACKGROUND);
unsigned long currentTime = millis();
String runningTimeStr;
String coolingTimeStr;
if (sys.timerActive && sys.status == STATUS_RUNNING) {
if (!sys.isCooling) {
unsigned long elapsed = currentTime - sys.startTime;
unsigned long remainingTime = (elapsed < sys.runningTime) ?
(sys.runningTime - elapsed) : 0;
runningTimeStr = formatTime(remainingTime);
coolingTimeStr = formatTime(sys.coolingTime);
} else {
unsigned long elapsed = currentTime - sys.startTime;
unsigned long coolingRemainingTime = (elapsed < sys.coolingTime) ?
(sys.coolingTime - elapsed) : 0;
runningTimeStr = "0'00";
coolingTimeStr = formatTime(coolingRemainingTime);
}
} else {
runningTimeStr = formatTime(sys.runningTime);
coolingTimeStr = formatTime(sys.coolingTime);
}
tft.setTextSize(2);
tft.setTextColor(ILI9341_WHITE);
tft.setCursor(10, 210);
tft.print(runningTimeStr);
tft.setCursor(100, 210);
tft.print("+");
tft.setCursor(135, 210);
tft.print(coolingTimeStr);
}
void drawControlIcons() {
// FIXED: Clear control area properly
tft.fillRect(200, 205, 120, 25, COLOR_BACKGROUND);
int iconWidth = 15;
int iconHeight = 15;
// Play/Pause button
int playX = 210;
int playY = 210;
uint16_t playBgColor = COLOR_BACKGROUND;
if (sys.selectedField == FIELD_PLAY_PAUSE) {
playBgColor = sys.editMode ? COLOR_FIELD_EDIT : COLOR_FIELD_SELECTED;
}
tft.fillRect(playX - 2, playY - 2, iconWidth + 4, iconHeight + 4, playBgColor);
if (sys.status == STATUS_RUNNING) {
// Draw Pause icon (two bars)
tft.fillRect(playX, playY, 5, iconHeight, ILI9341_GREEN);
tft.fillRect(playX + 10, playY, 5, iconHeight, ILI9341_GREEN);
} else {
// Draw Play icon (triangle)
tft.fillTriangle(playX, playY, playX, playY + iconHeight,
playX + iconWidth, playY + iconHeight/2, ILI9341_GREEN);
}
// Next button (segitiga ganda)
int nextX = 250;
int nextY = 210;
uint16_t nextBgColor = COLOR_BACKGROUND;
if (sys.selectedField == FIELD_NEXT) {
nextBgColor = sys.editMode ? COLOR_FIELD_EDIT : COLOR_FIELD_SELECTED;
}
tft.fillRect(nextX - 2, nextY - 2, iconWidth + 4, iconHeight + 4, nextBgColor);
// Draw double triangle (>>)
tft.fillTriangle(nextX, nextY, nextX, nextY + iconHeight,
nextX + iconWidth/2, nextY + iconHeight/2, ILI9341_CYAN);
tft.fillTriangle(nextX + iconWidth/2, nextY, nextX + iconWidth/2, nextY + iconHeight,
nextX + iconWidth, nextY + iconHeight/2, ILI9341_CYAN);
// Stop button
int stopX = 290;
int stopY = 210;
int stopSize = 15;
uint16_t stopBgColor = COLOR_BACKGROUND;
if (sys.selectedField == FIELD_STOP) {
stopBgColor = sys.editMode ? COLOR_FIELD_EDIT : COLOR_FIELD_SELECTED;
}
tft.fillRect(stopX - 2, stopY - 2, stopSize + 4, stopSize + 4, stopBgColor);
tft.fillRect(stopX, stopY, stopSize, stopSize, ILI9341_RED);
tft.drawRect(stopX, stopY, stopSize, stopSize, ILI9341_WHITE);
}
void drawManualPage() {
tft.fillScreen(COLOR_BACKGROUND);
drawPresetInfo();
drawRPMDisplay();
drawStatusDisplay();
drawCycleField();
drawVoltageBox();
drawTargetRPM();
drawGraphBox();
drawRotationMode();
drawTimers();
drawControlIcons();
drawLogField(); // Tambahkan field log
}
// ===== MAIN FUNCTIONS =====
void setup() {
Serial.begin(115200);
tft.begin();
tft.setRotation(1);
tft.fillScreen(COLOR_BACKGROUND);
pinMode(ENC_A, INPUT_PULLUP);
pinMode(ENC_B, INPUT_PULLUP);
pinMode(ENC_SW, INPUT_PULLUP);
pinMode(HALL_SENSOR_PIN, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(HALL_SENSOR_PIN), countPulse, FALLING);
// Initialize system
sys.currentPage = PAGE_MAIN_MENU;
sys.selectedMenuItem = 0;
sys.selectedField = FIELD_VOLTAGE;
sys.editMode = false;
sys.currentRPM = 0;
sys.maxRPM = 0;
sys.temperature = 25;
sys.runningTime = 120 * 60 * 1000;
sys.coolingTime = 0;
sys.timerActive = false;
sys.isCooling = false;
sys.lastTimerUpdate = 0;
sys.lastRPMUpdate = 0;
initializePresets();
loadManualDefaults();
drawMainMenu();
Serial.println("System initialized - FINAL VERSION");
}
void loop() {
// Baca perubahan encoder
int encoderDelta = readEncoder();
// Handle button press
if (readSWClick()) {
if (sys.currentPage == PAGE_MAIN_MENU) {
// Handle menu selection
if (strcmp(menuItems[sys.selectedMenuItem], "Manual") == 0) {
sys.currentPage = PAGE_MANUAL;
loadManualDefaults();
sys.editMode = true; // Langsung masuk edit mode untuk voltage
sys.selectedField = FIELD_VOLTAGE;
if (!sys.timerActive) {
startTimers(120, 0);
}
drawManualPage();
Serial.println("Entered Manual mode");
}
else if (strcmp(menuItems[sys.selectedMenuItem], "Program") == 0) {
sys.currentPage = PAGE_PROGRAM;
// drawProgramPage(); // Akan diimplementasikan nanti
Serial.println("Entered Program mode");
}
else if (strcmp(menuItems[sys.selectedMenuItem], "Log") == 0) {
sys.currentPage = PAGE_LOG;
// drawLogPage(); // Akan diimplementasikan nanti
Serial.println("Entered Log mode");
}
else if (strcmp(menuItems[sys.selectedMenuItem], "Setting") == 0) {
sys.currentPage = PAGE_SETTING;
// drawSettingPage(); // Akan diimplementasikan nanti
Serial.println("Entered Setting mode");
}
}
else if (sys.currentPage == PAGE_MANUAL) {
// Handle field selection in manual mode
switch (sys.selectedField) {
case FIELD_LOG:
// Pindah ke halaman log
sys.currentPage = PAGE_LOG;
// drawLogPage(); // Akan diimplementasikan nanti
Serial.println("Switched to Log page");
break;
case FIELD_PLAY_PAUSE:
// Toggle play/pause
if (sys.status == STATUS_RUNNING) {
sys.status = STATUS_PAUSED;
} else {
sys.status = STATUS_RUNNING;
}
drawStatusDisplay();
drawControlIcons();
Serial.println("Play/Pause toggled");
break;
case FIELD_NEXT:
// Next button action
// Tambahkan logika next step/cycle di sini
Serial.println("Next button pressed");
break;
case FIELD_STOP:
// Stop button action
sys.status = STATUS_STOPPED;
sys.timerActive = false;
drawStatusDisplay();
drawControlIcons();
Serial.println("Stopped");
break;
default:
// Toggle edit mode untuk field yang bisa diedit
sys.editMode = !sys.editMode;
switch (sys.selectedField) {
case FIELD_VOLTAGE:
drawVoltageBox();
break;
case FIELD_ROTATION:
drawRotationMode();
break;
}
break;
}
}
else if (sys.currentPage == PAGE_LOG) {
// Kembali ke menu utama dari log
sys.currentPage = PAGE_MAIN_MENU;
drawMainMenu();
Serial.println("Returned to Main Menu");
}
}
// Handle encoder rotation
if (encoderDelta != 0) {
if (sys.currentPage == PAGE_MAIN_MENU) {
handleMenuNavigation(encoderDelta);
drawMainMenu();
}
else if (sys.currentPage == PAGE_MANUAL) {
handleManualNavigation(encoderDelta);
// Update tampilan berdasarkan perubahan
if (!sys.editMode) {
// Mode navigasi - perbarui semua field untuk menunjukkan seleksi
drawVoltageBox();
drawRotationMode();
drawLogField();
drawControlIcons();
} else {
// Mode edit - hanya perbarui field yang sedang diedit
switch (sys.selectedField) {
case FIELD_VOLTAGE:
drawVoltageBox();
break;
case FIELD_ROTATION:
drawRotationMode();
break;
}
}
}
}
// Update background processes
if (sys.currentPage == PAGE_MANUAL) {
updateTimers();
updateRPM();
// Update suhu setiap 5 detik
static unsigned long lastTempUpdate = 0;
if (millis() - lastTempUpdate > 5000) {
sys.temperature = 25 + random(-5, 15); // Simulasi perubahan suhu
drawPresetInfo();
lastTempUpdate = millis();
}
// Update tampilan timer dan RPM setiap detik
static unsigned long lastDisplayUpdate = 0;
if (millis() - lastDisplayUpdate > 1000) {
drawTimers();
drawRPMDisplay();
lastDisplayUpdate = millis();
}
}
delay(20); // Delay kecil untuk stabilitas
}