#include <Wire.h>
#include <Adafruit_GFX.h> // Library grafis Adafruit
#include <Adafruit_SSD1306.h> // Library untuk OLED SSD1306
#include <INA226_WE.h>
#include <Preferences.h>
// --- DEKLARASI PIN ESP32 ---
// Deklarasi untuk OLED 0.96 inci (SSD1306)
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
// 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; // 5 menit untuk manual ON
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;
// --- VARIABEL UNTUK DETEKSI DOUBLE-CLICK ---
volatile unsigned long lastClickTime = 0;
volatile int clickCount = 0;
const long DOUBLE_CLICK_TIME_MS = 400; // Waktu maksimum antara dua klik untuk dianggap double-click
bool blinkState = true;
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 = 2000; // DIUBAH DARI 1000 MENJADI 2000 (2 detik)
// --- VARIABEL UNTUK OPTIMALISASI UPDATE OLED ---
char lastLine0[25] = "";
char lastLine1[25] = "";
char lastLine2[25] = "";
char lastLine3[25] = "";
ModeAlat lastMode = MODE_NORMAL;
int lastSelectedSetting = -1;
float lastTeganganAki = -1.0;
bool displayNeedsUpdate = true;
bool lastRelayStateDisplayed = false;
// Variabel untuk mengontrol frekuensi update OLED
unsigned long lastOLEDUpdateTime = 0;
const long OLED_UPDATE_INTERVAL_MS = 200;
// --- PROTOTYPE FUNGSI ---
void IRAM_ATTR readEncoder();
void IRAM_ATTR handleButtonInterrupt();
void saveSettings();
void loadSettings();
void updateOLEDDisplay();
void showTempMessage(const char* line1, const char* line2); // Fungsi untuk pesan sementara
// --- FUNGSI SETUP ---
void setup() {
Serial.begin(115200);
// --- Inisialisasi OLED ---
Wire.begin();
Wire.setClock(400000);
if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
Serial.println(F("SSD1306 alokasi gagal"));
for(;;);
}
Serial.println("Inisialisasi OLED...");
display.display();
delay(2000);
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(0, 0);
display.print("Inisialisasi...");
display.display();
Serial.println("Inisialisasi OLED selesai.");
delay(1000);
// --- Inisialisasi INA226 ---
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);
lastRelayStateDisplayed = (digitalRead(RELAY_PIN) == HIGH);
// --- 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;
display.clearDisplay();
displayNeedsUpdate = true;
}
// --- FUNGSI LOOP ---
void loop() {
unsigned long currentTime = millis();
// --- BACA SENSOR ---
teganganAki = ina226.getBusVoltage_V();
// Serial debugging ini bisa sangat sering, jadi saya komen untuk mengurangi output di Serial Monitor
// 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)");
// Periksa apakah tegangan aki berubah signifikan untuk update OLED
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);
} 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);
} else if (currentMode == MODE_SETTING_SELECT) {
selectedSetting += numClicks;
if (selectedSetting < 0) selectedSetting = 1;
if (selectedSetting > 1) selectedSetting = 0;
}
displayNeedsUpdate = true;
}
lastEncoderPos = encoderPos;
}
// --- LOGIKA KONTROL RELAY ---
// Logika utama kontrol relay hanya aktif di MODE_NORMAL
if (currentMode == MODE_NORMAL) {
// Timeout untuk manualRelayOn (jika melebihi batas waktu)
if (manualRelayOn && (currentTime - manualControlStartTime > MANUAL_CONTROL_TIMEOUT)) {
manualRelayOn = false;
Serial.println("Kontrol manual relay nonaktif (timeout).");
digitalWrite(RELAY_PIN, LOW);
displayNeedsUpdate = true;
relayOffDelayActive = false; // Pastikan delay OFF juga direset
showTempMessage("Relay Timeout", "OFF Otomatis!");
}
// Selalu cek batas atas, bahkan jika manualRelayOn
if (teganganAki >= batasTeganganAtas) {
if (!relayOffDelayActive) { // Mulai delay jika belum aktif
relayOffDelayStartTime = currentTime;
relayOffDelayActive = true;
Serial.println("Tegangan tinggi terdeteksi. Memulai hitung mundur untuk OFF relay...");
// showTempMessage("Voltase Tinggi", "Relay Akan OFF!"); // <<< DIKOMENTARI/DIHILANGKAN
displayNeedsUpdate = true;
}
if (relayOffDelayActive && (currentTime - relayOffDelayStartTime >= RELAY_OFF_DELAY_MS)) {
if (digitalRead(RELAY_PIN) == HIGH) {
digitalWrite(RELAY_PIN, LOW);
Serial.println("Relay OTOMATIS OFF (Tegangan tinggi setelah delay).");
manualRelayOn = false; // Matikan mode manual jika diputus otomatis
showTempMessage("Relay OFF", "Voltase Tinggi!");
displayNeedsUpdate = true;
}
relayOffDelayActive = false;
}
} else { // Jika tegangan di bawah batas atas
if (relayOffDelayActive) { // Jika delay aktif tapi tegangan sudah kembali normal
relayOffDelayActive = false;
Serial.println("Delay OFF relay dibatalkan, tegangan kembali normal.");
showTempMessage("OFF Delay", "Dibatalkan!");
displayNeedsUpdate = true;
}
// Logika utama ON/OFF otomatis hanya jika tidak dalam mode manual
if (!manualRelayOn) {
if (teganganAki < batasTeganganBawah) {
if (digitalRead(RELAY_PIN) == LOW) {
digitalWrite(RELAY_PIN, HIGH);
Serial.println("Relay OTOMATIS ON (Tegangan rendah).");
displayNeedsUpdate = true;
}
} else {
// Jika tegangan di antara batas bawah dan batas atas,
// dan tidak ada kondisi khusus untuk ON/OFF, biarkan status relay saat ini
// Ini adalah area histeresis, relay akan tetap pada status sebelumnya
// kecuali ada pemicu baru (seperti double-click)
}
} else { // Jika manualRelayOn == true
if (digitalRead(RELAY_PIN) == LOW) { // Jika seharusnya ON tapi mati
digitalWrite(RELAY_PIN, HIGH); // Paksa ON
Serial.println("Relay MANUAL DIPAKSA ON (oleh double-click).");
displayNeedsUpdate = true;
}
// Timeout untuk manualRelayOn sudah di atas
}
}
// Pastikan lastRelayStateDisplayed diperbarui dan memicu displayNeedsUpdate
bool newRelayState = (digitalRead(RELAY_PIN) == HIGH);
if (newRelayState != lastRelayStateDisplayed) {
displayNeedsUpdate = true;
lastRelayStateDisplayed = newRelayState;
Serial.print("Relay state changed! New state: "); Serial.println(newRelayState ? "ON" : "OFF");
}
} else { // currentMode != MODE_NORMAL (mode pengaturan)
if (digitalRead(RELAY_PIN) == HIGH) {
digitalWrite(RELAY_PIN, LOW);
Serial.println("Relay OFF (Di mode pengaturan).");
displayNeedsUpdate = true;
}
manualRelayOn = false; // Pastikan manualRelayOn mati saat di mode pengaturan
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)");
selectedSetting = 0;
displayNeedsUpdate = true;
} else {
currentMode = MODE_NORMAL;
saveSettings();
Serial.println("Mode: Normal (Long Press Keluar, Pengaturan Disimpan)");
messageDisplayStartTime = currentTime;
displayNeedsUpdate = true;
}
lastButtonStateChangeTime = currentTime;
}
} else { // Tombol dilepas
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) & DOUBLE-CLICK ---
if (shortPressDetected) {
shortPressDetected = false; // Reset short press
if (currentMode == MODE_NORMAL) {
// Deteksi Double-Click
if (currentTime - lastClickTime < DOUBLE_CLICK_TIME_MS) {
clickCount++;
if (clickCount == 2) { // Double-click terdeteksi!
Serial.println("Double-Click Terdeteksi di Mode Normal!");
manualRelayOn = !manualRelayOn; // Toggle manual relay state
manualControlStartTime = currentTime; // Reset timeout
if (manualRelayOn) {
showTempMessage("Manual ON!", "Batas Aman Aktif!");
} else {
showTempMessage("Manual OFF!", "Otomatis Aktif!");
}
displayNeedsUpdate = true;
clickCount = 0; // Reset click count
lastClickTime = 0; // Reset click time
}
} else { // Klik pertama atau waktu double-click terlewat
clickCount = 1;
lastClickTime = currentTime;
}
} else 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");
}
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");
displayNeedsUpdate = true;
}
}
// Reset clickCount jika waktu double-click terlewat dan belum ada double-click
if (clickCount == 1 && (currentTime - lastClickTime > DOUBLE_CLICK_TIME_MS)) {
Serial.println("Single click detected (double-click timeout).");
clickCount = 0;
lastClickTime = 0;
}
// --- UPDATE TAMPILAN OLED ---
if (messageDisplayStartTime != 0 && (currentTime - messageDisplayStartTime < MESSAGE_DISPLAY_DURATION)) {
// Pesan sementara sedang ditampilkan, tidak perlu panggil updateOLEDDisplay
} else {
if (currentTime - lastOLEDUpdateTime >= OLED_UPDATE_INTERVAL_MS) {
updateOLEDDisplay();
lastOLEDUpdateTime = currentTime;
messageDisplayStartTime = 0;
}
}
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) { // Tombol Ditekan
if (!buttonIsCurrentlyPressed) {
buttonIsCurrentlyPressed = true;
pushButtonPressTime = currentTime;
}
} else { // Tombol Dilepas
buttonIsCurrentlyPressed = false;
}
}
// --- FUNGSI UPDATE TAMPILAN OLED ---
void updateOLEDDisplay() {
// Serial.println("updateOLEDDisplay() called.");
char currentLine0[25];
char currentLine1[25];
char currentLine2[25];
char currentLine3[25];
blinkState = true;
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
if (currentMode == MODE_NORMAL) {
snprintf(currentLine0, sizeof(currentLine0), "Volt Aki: %.2f V", teganganAki);
const char* relayStatus = (digitalRead(RELAY_PIN) == HIGH) ? "ON" : "OFF";
const char* manualStatus = manualRelayOn ? "(M)" : "(A)";
snprintf(currentLine1, sizeof(currentLine1), "RELAY: %s %s", relayStatus, manualStatus);
snprintf(currentLine2, sizeof(currentLine2), "Batas Atas: %.2f V", batasTeganganAtas);
snprintf(currentLine3, sizeof(currentLine3), "Batas Bawah: %.2f V", batasTeganganBawah);
} else if (currentMode == MODE_SETTING_SELECT) {
snprintf(currentLine0, sizeof(currentLine0), "PILIH PENGATURAN");
snprintf(currentLine1, sizeof(currentLine1), "");
if (selectedSetting == 0) {
snprintf(currentLine2, sizeof(currentLine2), "> Batas Bawah");
snprintf(currentLine3, sizeof(currentLine3), " Batas Atas");
} else {
snprintf(currentLine2, sizeof(currentLine2), " Batas Bawah");
snprintf(currentLine3, sizeof(currentLine3), "> Batas Atas");
}
} else if (currentMode == MODE_SETTING_BAWAH_EDIT) {
snprintf(currentLine0, sizeof(currentLine0), "EDIT BATAS BAWAH");
snprintf(currentLine1, sizeof(currentLine1), "");
snprintf(currentLine2, sizeof(currentLine2), "%.2f V", batasTeganganBawah);
snprintf(currentLine3, sizeof(currentLine3), "");
} else if (currentMode == MODE_SETTING_ATAS_EDIT) {
snprintf(currentLine0, sizeof(currentLine0), "EDIT BATAS ATAS ");
snprintf(currentLine1, sizeof(currentLine1), "");
snprintf(currentLine2, sizeof(currentLine2), "%.2f V", batasTeganganAtas);
snprintf(currentLine3, sizeof(currentLine3), "");
}
// --- HANYA TULIS KE OLED JIKA ADA PERUBAHAN ATAU DIPAKSA UPDATE ---
if (currentMode != lastMode || displayNeedsUpdate ||
(strcmp(currentLine0, lastLine0) != 0) ||
(strcmp(currentLine1, lastLine1) != 0) ||
(strcmp(currentLine2, lastLine2) != 0) ||
(strcmp(currentLine3, lastLine3) != 0))
{
Serial.println(" OLED actually updated due to change.");
display.clearDisplay();
display.setCursor(0, 0);
display.print(currentLine0);
display.setCursor(0, 16);
display.print(currentLine1);
if (SCREEN_HEIGHT >= 64) {
display.setCursor(0, 32);
display.print(currentLine2);
display.setCursor(0, 48);
display.print(currentLine3);
}
display.display();
strcpy(lastLine0, currentLine0);
strcpy(lastLine1, currentLine1);
strcpy(lastLine2, currentLine2);
strcpy(lastLine3, currentLine3);
displayNeedsUpdate = false;
} else {
// Serial.println(" OLED NOT updated (no significant change detected).");
}
lastMode = currentMode;
lastSelectedSetting = selectedSetting;
lastTeganganAki = teganganAki;
}
// --- FUNGSI UNTUK MENAMPILKAN PESAN SEMENTARA DI OLED ---
void showTempMessage(const char* line1, const char* line2) {
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(0, (SCREEN_HEIGHT - 16) / 2);
display.print(line1);
display.setCursor(0, (SCREEN_HEIGHT - 16) / 2 + 16);
display.print(line2);
display.display();
messageDisplayStartTime = millis();
}
// --- 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.");
showTempMessage("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.");
}