#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <INA226_WE.h>
#include <Preferences.h>
// --- DEKLARASI PIN ESP32 ---
LiquidCrystal_I2C lcd(0x27, 16, 2);
// Pin untuk Rotary Encoder
#define ROTARY_A_PIN 32
#define ROTARY_B_PIN 33
#define ROTARY_SW_PIN 25
// Pin untuk Relay
#define RELAY_PIN 26
// --- OBJEK SENSOR ---
INA226_WE ina226;
// --- OBJEK NVS ---
Preferences preferences;
// --- PARAMETER AKI & BATAS ---
float batasTeganganBawah = 10.80;
float batasTeganganAtas = 14.80;
// --- VARIABEL PENGUKURAN ---
float teganganAki = 0.0;
// --- VARIABEL KONTROL MODE & UI ---
enum ModeAlat {
MODE_NORMAL,
MODE_SETTING_SELECT,
MODE_SETTING_BAWAH_EDIT,
MODE_SETTING_ATAS_EDIT
};
ModeAlat currentMode = MODE_NORMAL;
bool manualRelayOn = false;
unsigned long manualControlStartTime = 0;
const long MANUAL_CONTROL_TIMEOUT = 300000;
volatile int encoderPos = 0;
int lastEncoderPos = 0;
volatile int lastEncoded = 0;
const int ENCODER_DEBOUNCE_DELAY = 1;
const int ENCODER_STEPS_PER_CLICK = 4;
int encoderClickCounter = 0;
volatile unsigned long pushButtonPressTime = 0;
volatile bool buttonIsCurrentlyPressed = false;
unsigned long lastButtonStateChangeTime = 0;
const long BUTTON_DEBOUNCE_MS = 150;
volatile bool shortPressDetected = false;
volatile bool longPressDetected = false;
const long LONG_PRESS_DURATION = 3000;
bool blinkState = false;
unsigned long lastBlinkMillis = 0;
const long BLINK_INTERVAL = 500;
int selectedSetting = 0;
unsigned long messageDisplayStartTime = 0;
const long MESSAGE_DISPLAY_DURATION = 1000;
// --- VARIABEL UNTUK DELAY RELAY OFF ---
unsigned long relayOffDelayStartTime = 0;
bool relayOffDelayActive = false;
const long RELAY_OFF_DELAY_MS = 15000;
// --- VARIABEL UNTUK OPTIMALISASI UPDATE LCD ---
char lastLine0[17] = "";
char lastLine1[17] = "";
ModeAlat lastMode = MODE_NORMAL;
int lastSelectedSetting = -1;
float lastTeganganAki = -1.0;
bool displayNeedsUpdate = true;
// --- Tambahkan variabel untuk melacak status relay yang terakhir ditampilkan ---
// Asumsi: HIGH = ON, LOW = OFF. SESUAIKAN JIKA MODUL RELAY ANDA AKTIF LOW.
// Jika aktif LOW, maka true = LOW (ON), false = HIGH (OFF)
bool lastRelayStateDisplayed = false;
// --- PROTOTYPE FUNGSI ---
void IRAM_ATTR readEncoder();
void IRAM_ATTR handleButtonInterrupt();
void saveSettings();
void loadSettings();
void updateLCDDisplay();
// --- FUNGSI SETUP ---
void setup() {
Serial.begin(115200);
// --- Inisialisasi LCD ---
lcd.init();
lcd.backlight();
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Inisialisasi...");
Serial.println("Inisialisasi LCD...");
delay(1000);
// --- Inisialisasi INA226 ---
Wire.begin();
Wire.setClock(50000);
ina226.init();
Serial.println("INA226 ditemukan (asumsi koneksi OK).");
ina226.setAverage(AVERAGE_4);
ina226.setConversionTime(CONV_TIME_1100);
ina226.setMeasureMode(CONTINOUS);
// --- Inisialisasi Rotary Encoder ---
pinMode(ROTARY_A_PIN, INPUT_PULLUP);
pinMode(ROTARY_B_PIN, INPUT_PULLUP);
pinMode(ROTARY_SW_PIN, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(ROTARY_A_PIN), readEncoder, CHANGE);
attachInterrupt(digitalPinToInterrupt(ROTARY_B_PIN), readEncoder, CHANGE);
attachInterrupt(digitalPinToInterrupt(ROTARY_SW_PIN), handleButtonInterrupt, CHANGE);
// --- Inisialisasi Relay ---
pinMode(RELAY_PIN, OUTPUT);
digitalWrite(RELAY_PIN, LOW); // Pastikan relay OFF saat startup (jika aktif HIGH)
// Ubah menjadi HIGH jika modul Anda aktif LOW
// Inisialisasi status relay yang ditampilkan
lastRelayStateDisplayed = (digitalRead(RELAY_PIN) == HIGH); // Sesuaikan ini dengan jenis relay Anda
// --- Muat Pengaturan dari NVS ---
loadSettings();
Serial.print("Batas Bawah: "); Serial.println(batasTeganganBawah, 2);
Serial.print("Batas Atas: "); Serial.println(batasTeganganAtas, 2);
int initialEncoderAPinState = digitalRead(ROTARY_A_PIN);
int initialEncoderBPinState = digitalRead(ROTARY_B_PIN);
lastEncoded = (initialEncoderAPinState << 1) | initialEncoderBPinState;
lcd.clear();
displayNeedsUpdate = true; // Paksa update pertama setelah setup
}
// --- FUNGSI LOOP ---
void loop() {
unsigned long currentTime = millis();
// --- BACA SENSOR ---
teganganAki = ina226.getBusVoltage_V();
Serial.print("Volt Aki Terbaca: ");
Serial.print(teganganAki, 3);
Serial.print(" V | Batas Atas: ");
Serial.print(batasTeganganAtas, 3);
Serial.print(" V | Batas Bawah: ");
Serial.print(batasTeganganBawah, 3);
Serial.print(" V | Status Pin RELAY: ");
Serial.println(digitalRead(RELAY_PIN) == HIGH ? "HIGH (ON)" : "LOW (OFF)"); // Sesuaikan pesan ini dengan relay Anda
// Periksa apakah tegangan aki berubah signifikan untuk update LCD
if (abs(teganganAki - lastTeganganAki) > 0.01) {
displayNeedsUpdate = true;
}
// Tangani putaran Rotary Encoder
if (encoderPos != lastEncoderPos) {
int encoderDelta = encoderPos - lastEncoderPos;
encoderClickCounter += encoderDelta;
if (abs(encoderClickCounter) >= ENCODER_STEPS_PER_CLICK) {
int numClicks = encoderClickCounter / ENCODER_STEPS_PER_CLICK;
encoderClickCounter %= ENCODER_STEPS_PER_CLICK;
if (currentMode == MODE_SETTING_BAWAH_EDIT) {
batasTeganganBawah += (float)numClicks * 0.01;
batasTeganganBawah = round(batasTeganganBawah * 100.0) / 100.0;
batasTeganganBawah = constrain(batasTeganganBawah, 0.0, 36.0);
blinkState = true;
lastBlinkMillis = currentTime;
} else if (currentMode == MODE_SETTING_ATAS_EDIT) {
batasTeganganAtas += (float)numClicks * 0.01;
batasTeganganAtas = round(batasTeganganAtas * 100.0) / 100.0;
batasTeganganAtas = constrain(batasTeganganAtas, 0.0, 36.0);
blinkState = true;
lastBlinkMillis = currentTime;
} else if (currentMode == MODE_SETTING_SELECT) {
selectedSetting += numClicks;
if (selectedSetting < 0) selectedSetting = 1;
if (selectedSetting > 1) selectedSetting = 0;
blinkState = true;
}
displayNeedsUpdate = true;
}
lastEncoderPos = encoderPos;
}
// --- LOGIKA KONTROL RELAY ---
if (currentMode == MODE_NORMAL) {
if (manualRelayOn && (currentTime - manualControlStartTime > MANUAL_CONTROL_TIMEOUT)) {
manualRelayOn = false;
Serial.println("Kontrol manual relay nonaktif (timeout).");
digitalWrite(RELAY_PIN, LOW); // Sesuaikan ini jika relay Anda aktif LOW
displayNeedsUpdate = true;
relayOffDelayActive = false;
}
if (manualRelayOn) {
if (teganganAki >= batasTeganganBawah && teganganAki <= batasTeganganAtas) {
if (digitalRead(RELAY_PIN) == LOW) { // Sesuaikan ini jika relay Anda aktif LOW
digitalWrite(RELAY_PIN, HIGH); // Sesuaikan ini jika relay Anda aktif LOW
Serial.println("Relay MANUAL ON.");
}
relayOffDelayActive = false;
} else {
if (digitalRead(RELAY_PIN) == HIGH) { // Sesuaikan ini jika relay Anda aktif LOW
digitalWrite(RELAY_PIN, LOW); // Sesuaikan ini jika relay Anda aktif LOW
Serial.println("Relay MANUAL OFF karena di luar batas aman.");
}
manualRelayOn = false;
displayNeedsUpdate = true;
relayOffDelayActive = false;
lcd.clear();
lcd.setCursor(0,0); lcd.print("Tegangan Di Luar");
lcd.setCursor(0,1); lcd.print("Batas Aman!");
messageDisplayStartTime = currentTime;
}
} else {
// Periksa perubahan status relay di sini untuk memicu update LCD
bool currentRelayPhysicalState = (digitalRead(RELAY_PIN) == HIGH); // Asumsi HIGH = ON
// ^^^ Sesuaikan ini dengan jenis relay Anda: `(digitalRead(RELAY_PIN) == LOW)` jika aktif LOW
if (teganganAki < batasTeganganBawah) { // Nyalakan relay jika tegangan rendah
if (digitalRead(RELAY_PIN) == LOW) { // Asumsi: LOW = OFF. Ubah ke HIGH jika aktif LOW
digitalWrite(RELAY_PIN, HIGH); // Asumsi: HIGH = ON. Ubah ke LOW jika aktif LOW
Serial.println("Relay OTOMATIS ON (Tegangan rendah).");
displayNeedsUpdate = true; // Paksa update LCD
}
relayOffDelayActive = false;
} else if (teganganAki >= batasTeganganAtas) { // Mematikan relay jika tegangan mencapai/melebihi batas atas
// Perhatikan operator >=
if (!relayOffDelayActive) {
relayOffDelayStartTime = currentTime;
relayOffDelayActive = true;
Serial.println("Tegangan tinggi terdeteksi. Memulai hitung mundur untuk OFF relay...");
displayNeedsUpdate = true; // Paksa update LCD (opsional, bisa juga hanya di akhir delay)
}
if (relayOffDelayActive && (currentTime - relayOffDelayStartTime >= RELAY_OFF_DELAY_MS)) {
if (digitalRead(RELAY_PIN) == HIGH) { // Asumsi: HIGH = ON. Ubah ke LOW jika aktif LOW
digitalWrite(RELAY_PIN, LOW); // Asumsi: LOW = OFF. Ubah ke HIGH jika aktif LOW
Serial.println("Relay OTOMATIS OFF (Tegangan tinggi setelah delay).");
displayNeedsUpdate = true; // Paksa update LCD
}
relayOffDelayActive = false;
}
} else {
// Tegangan di antara batas bawah dan batas atas (area histeresis)
// Relay tidak akan berubah statusnya di sini kecuali jika kondisi sebelumnya memaksa perubahan.
// Penting: Pastikan delay dimatikan jika tegangan kembali ke area normal/aman sebelum timeout
if (relayOffDelayActive) { // Jika delay aktif tapi tegangan sudah kembali normal
relayOffDelayActive = false;
Serial.println("Delay OFF relay dibatalkan, tegangan kembali normal.");
displayNeedsUpdate = true; // Paksa update LCD jika delay dibatalkan
}
}
// Pastikan lastRelayStateDisplayed diperbarui dan memicu displayNeedsUpdate
bool newRelayState = (digitalRead(RELAY_PIN) == HIGH); // Asumsi HIGH = ON. Ubah ke LOW jika aktif LOW
if (newRelayState != lastRelayStateDisplayed) {
displayNeedsUpdate = true;
lastRelayStateDisplayed = newRelayState;
Serial.print("Relay state changed! New state: "); Serial.println(newRelayState ? "ON" : "OFF");
}
} // End of manualRelayOn else
} else { // currentMode != MODE_NORMAL (mode pengaturan)
if (digitalRead(RELAY_PIN) == HIGH) { // Asumsi: HIGH = ON. Ubah ke LOW jika aktif LOW
digitalWrite(RELAY_PIN, LOW); // Asumsi: LOW = OFF. Ubah ke HIGH jika aktif LOW
Serial.println("Relay OFF (Di mode pengaturan).");
displayNeedsUpdate = true;
}
manualRelayOn = false;
displayNeedsUpdate = true;
relayOffDelayActive = false;
}
// --- LOGIKA PENANGANAN TOMBOL PUSH ROTARY ---
if (buttonIsCurrentlyPressed) {
if (!longPressDetected && (currentTime - pushButtonPressTime >= LONG_PRESS_DURATION)) {
longPressDetected = true;
shortPressDetected = false;
if (currentMode == MODE_NORMAL) {
currentMode = MODE_SETTING_SELECT;
Serial.println("Mode: Pilih Pengaturan (Long Press)");
blinkState = true;
selectedSetting = 0;
displayNeedsUpdate = true;
} else {
currentMode = MODE_NORMAL;
saveSettings();
Serial.println("Mode: Normal (Long Press Keluar, Pengaturan Disimpan)");
messageDisplayStartTime = currentTime;
blinkState = true;
displayNeedsUpdate = true;
}
lastButtonStateChangeTime = currentTime;
}
} else {
if (pushButtonPressTime != 0 && (currentTime - pushButtonPressTime < LONG_PRESS_DURATION) && !longPressDetected) {
if (currentTime - lastButtonStateChangeTime > BUTTON_DEBOUNCE_MS) {
shortPressDetected = true;
lastButtonStateChangeTime = currentTime;
}
}
pushButtonPressTime = 0;
longPressDetected = false;
}
// --- TANGANI AKSI SHORT PRESS (SETELAH DEBOUNCE) ---
if (shortPressDetected) {
shortPressDetected = false;
// Logika untuk pengaturan (TETAP AKTIF)
if (currentMode == MODE_SETTING_SELECT) {
if (selectedSetting == 0) {
currentMode = MODE_SETTING_BAWAH_EDIT;
Serial.println("Mode: Edit Batas Bawah");
} else {
currentMode = MODE_SETTING_ATAS_EDIT;
Serial.println("Mode: Edit Batas Atas");
}
blinkState = true;
lastBlinkMillis = currentTime;
displayNeedsUpdate = true;
} else if (currentMode == MODE_SETTING_BAWAH_EDIT || currentMode == MODE_SETTING_ATAS_EDIT) {
currentMode = MODE_SETTING_SELECT;
Serial.println("Mode: Kembali ke Pilih Pengaturan");
blinkState = true;
displayNeedsUpdate = true;
}
}
// --- UPDATE TAMPILAN LCD ---
if (messageDisplayStartTime != 0 && (currentTime - messageDisplayStartTime < MESSAGE_DISPLAY_DURATION)) {
// Pesan sementara sedang ditampilkan, tidak perlu panggil updateLCDDisplay
} else {
updateLCDDisplay();
messageDisplayStartTime = 0; // Reset setelah waktu habis
}
delay(10);
}
// --- FUNGSI INTERRUPT ROTARY ENCODER ---
void IRAM_ATTR readEncoder() {
int encoderAPinState = digitalRead(ROTARY_A_PIN);
int encoderBPinState = digitalRead(ROTARY_B_PIN);
int encoded = (encoderAPinState << 1) | encoderBPinState;
if (encoded == 0b00) {
if (lastEncoded == 0b01) encoderPos--;
else if (lastEncoded == 0b10) encoderPos++;
} else if (encoded == 0b01) {
if (lastEncoded == 0b00) encoderPos++;
else if (lastEncoded == 0b11) encoderPos--;
} else if (encoded == 0b11) {
if (lastEncoded == 0b01) encoderPos++;
else if (lastEncoded == 0b10) encoderPos--;
} else if (encoded == 0b10) {
if (lastEncoded == 0b11) encoderPos++;
else if (lastEncoded == 0b00) encoderPos--;
}
lastEncoded = encoded;
}
// --- FUNGSI INTERRUPT TOMBOL PUSH ROTARY ---
void IRAM_ATTR handleButtonInterrupt() {
unsigned long currentTime = millis();
if (digitalRead(ROTARY_SW_PIN) == LOW) {
if (!buttonIsCurrentlyPressed) {
buttonIsCurrentlyPressed = true;
pushButtonPressTime = currentTime;
}
} else {
buttonIsCurrentlyPressed = false;
}
}
// --- FUNGSI UPDATE TAMPILAN LCD ---
void updateLCDDisplay() {
// Tambahkan ini di awal fungsi untuk debugging
Serial.println("updateLCDDisplay() called.");
char currentLine0[17];
char currentLine1[17];
if (currentMode == MODE_SETTING_BAWAH_EDIT || currentMode == MODE_SETTING_ATAS_EDIT) {
if (millis() - lastBlinkMillis > BLINK_INTERVAL) {
blinkState = !blinkState;
lastBlinkMillis = millis();
displayNeedsUpdate = true;
}
} else {
if (!blinkState) {
blinkState = true;
displayNeedsUpdate = true;
}
}
if (currentMode == MODE_NORMAL) {
snprintf(currentLine0, sizeof(currentLine0), "Volt Aki: %.1f V", teganganAki);
// Asumsi: HIGH = ON. Jika aktif LOW, ganti HIGH dengan LOW untuk mengecek status ON
if (digitalRead(RELAY_PIN) == HIGH) {
snprintf(currentLine1, sizeof(currentLine1), "RELAY: ON ");
} else {
snprintf(currentLine1, sizeof(currentLine1), "RELAY: OFF ");
}
} else if (currentMode == MODE_SETTING_SELECT) {
snprintf(currentLine0, sizeof(currentLine0), "PILIH PENGATURAN");
if (selectedSetting == 0) {
snprintf(currentLine1, sizeof(currentLine1), "> Batas Bawah ");
} else {
snprintf(currentLine1, sizeof(currentLine1), "> Batas Atas ");
}
} else if (currentMode == MODE_SETTING_BAWAH_EDIT) {
snprintf(currentLine0, sizeof(currentLine0), "EDIT BATAS BAWAH");
if (blinkState) {
snprintf(currentLine1, sizeof(currentLine1), "%.2f V", batasTeganganBawah);
} else {
snprintf(currentLine1, sizeof(currentLine1), " ");
}
} else if (currentMode == MODE_SETTING_ATAS_EDIT) {
snprintf(currentLine0, sizeof(currentLine0), "EDIT BATAS ATAS ");
if (blinkState) {
snprintf(currentLine1, sizeof(currentLine1), "%.2f V", batasTeganganAtas);
} else {
snprintf(currentLine1, sizeof(currentLine1), " ");
}
}
// --- HANYA TULIS KE LCD JIKA ADA PERUBAHAN ATAU DIPAKSA UPDATE ---
if (currentMode != lastMode || displayNeedsUpdate || (strcmp(currentLine0, lastLine0) != 0) || (strcmp(currentLine1, lastLine1) != 0)) {
Serial.println(" LCD actually updated due to change."); // Pesan ini jika LCD benar-benar ditulis ulang
lcd.setCursor(0, 0); for(int i=0; i<16; i++) lcd.print(" ");
lcd.setCursor(0, 1); for(int i=0; i<16; i++) lcd.print(" ");
lcd.setCursor(0, 0); lcd.print(currentLine0);
lcd.setCursor(0, 1); lcd.print(currentLine1);
strcpy(lastLine0, currentLine0);
strcpy(lastLine1, currentLine1);
displayNeedsUpdate = false;
}
lastMode = currentMode;
lastSelectedSetting = selectedSetting;
lastTeganganAki = teganganAki;
}
// --- FUNGSI SIMPAN/MUAT PENGATURAN KE NVS ---
void saveSettings() {
preferences.begin("charger_app", false);
preferences.putFloat("batasBawah", batasTeganganBawah);
preferences.putFloat("batasAtas", batasTeganganAtas);
preferences.end();
Serial.println("Pengaturan disimpan.");
}
void loadSettings() {
preferences.begin("charger_app", true);
batasTeganganBawah = preferences.getFloat("batasBawah", 10.80);
batasTeganganAtas = preferences.getFloat("batasAtas", 14.80);
preferences.end();
Serial.println("Pengaturan dimuat.");
}
No comments:
Post a Comment