Search This Blog

Monday, 14 July 2025

CARGER AKI CONTROLER INA226-LIBARY WE ROTARI SW+OLED 0.96 SUKSES TAMPILAN LEBIH BAIK

 #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.");

}

CARGER AKI CONTROLER INA226-LIBARY WE ROTARI SW+OLED 0.96 SUKSES

 #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)

// Ukuran layar OLED dalam piksel (biasanya 128x64 atau 128x32)

#define SCREEN_WIDTH 128 // OLED display width, in pixels

#define SCREEN_HEIGHT 64 // OLED display height, in pixels (atau 32 jika Anda menggunakan versi 128x32)


// Deklarasi pin reset OLED (biasanya terhubung ke GPIO, atau -1 jika terhubung ke VCC)

#define OLED_RESET -1 // Reset pin # (or -1 if sharing Arduino reset pin)


// Inisialisasi objek Adafruit_SSD1306 dengan lebar, tinggi, dan objek Wire

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;


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 = true; // Selalu true agar teks selalu terlihat (kedipan dihilangkan)

unsigned long lastBlinkMillis = 0;

const long BLINK_INTERVAL = 500; // Tidak lagi relevan untuk kedipan di mode pengaturan


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 = 1000; // Tetap 1 detik


// --- VARIABEL UNTUK OPTIMALISASI UPDATE OLED ---

// Untuk OLED, kita masih bisa menggunakan optimasi string

char lastLine0[25] = ""; // Lebih panjang karena OLED bisa menampilkan lebih banyak karakter per baris dengan font kecil

char lastLine1[25] = ""; 

char lastLine2[25] = ""; // Baris tambahan untuk OLED 128x64

char lastLine3[25] = ""; // Baris tambahan untuk OLED 128x64

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; // Update OLED setiap 200ms


// --- PROTOTYPE FUNGSI ---

void IRAM_ATTR readEncoder();

void IRAM_ATTR handleButtonInterrupt();

void saveSettings();

void loadSettings();

void updateOLEDDisplay(); // Ganti nama fungsi update


// --- FUNGSI SETUP ---

void setup() {

  Serial.begin(115200);


  // --- Inisialisasi OLED ---

  Wire.begin(); // Pastikan Wire (I2C) dimulai sebelum inisialisasi OLED

  Wire.setClock(400000); // OLED bisa bekerja lebih cepat, coba 400kHz untuk I2C


  // Inisialisasi display dengan alamat I2C 0x3C (bisa 0x3D juga, tergantung modul)

  // Periksa alamat I2C OLED Anda. Jika tidak berhasil dengan 0x3C, coba 0x3D

  if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { 

    Serial.println(F("SSD1306 alokasi gagal"));

    for(;;); // Jangan lanjutkan jika gagal

  }


  Serial.println("Inisialisasi OLED...");

  display.display(); // Tampilkan splash screen Adafruit

  delay(2000); 

  display.clearDisplay(); // Hapus buffer


  display.setTextSize(1);      // Ukuran teks 1 (6x8 piksel per karakter)

  display.setTextColor(SSD1306_WHITE); // Warna teks putih

  display.setCursor(0, 0);

  display.print("Inisialisasi...");

  display.display(); // Tampilkan teks ke layar

  Serial.println("Inisialisasi OLED selesai.");

  delay(1000);


  // --- Inisialisasi INA226 ---

  ina226.init(); // Sudah menggunakan Wire.begin() di atas


  Serial.println("INA226 ditemukan (asumsi koneksi OK).");


  ina226.setAverage(AVERAGE_4);

  ina226.setConversionTime(CONV_TIME_1100);

  ina226.setMeasureMode(CONTINOUS); // Perbaikan: ina26 -> ina226


  // --- 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(); // Bersihkan layar OLED

  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)"); 


  // 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 ---

  if (currentMode == MODE_NORMAL) {

    if (manualRelayOn && (currentTime - manualControlStartTime > MANUAL_CONTROL_TIMEOUT)) {

      manualRelayOn = false;

      Serial.println("Kontrol manual relay nonaktif (timeout).");

      digitalWrite(RELAY_PIN, LOW); 

      displayNeedsUpdate = true;

      relayOffDelayActive = false;

    }


    if (manualRelayOn) {

      if (teganganAki >= batasTeganganBawah && teganganAki <= batasTeganganAtas) {

        if (digitalRead(RELAY_PIN) == LOW) { 

          digitalWrite(RELAY_PIN, HIGH); 

          Serial.println("Relay MANUAL ON.");

        }

        relayOffDelayActive = false;

      } else {

        if (digitalRead(RELAY_PIN) == HIGH) { 

          digitalWrite(RELAY_PIN, LOW); 

          Serial.println("Relay MANUAL OFF karena di luar batas aman.");

        }

        manualRelayOn = false;

        displayNeedsUpdate = true;

        relayOffDelayActive = false;

        display.clearDisplay(); // Bersihkan OLED

        display.setCursor(0,0); display.print("Tegangan Di Luar");

        display.setCursor(0,16); display.print("Batas Aman!"); // Baris kedua di OLED (16 piksel ke bawah)

        display.display(); // Tampilkan perubahan

        messageDisplayStartTime = currentTime;

      }

    } else {

      bool currentRelayPhysicalState = (digitalRead(RELAY_PIN) == HIGH); 


      if (teganganAki < batasTeganganBawah) { 

        if (digitalRead(RELAY_PIN) == LOW) { 

          digitalWrite(RELAY_PIN, HIGH); 

          Serial.println("Relay OTOMATIS ON (Tegangan rendah).");

          displayNeedsUpdate = true; 

        }

        relayOffDelayActive = false;

      } else if (teganganAki >= batasTeganganAtas) { 

        if (!relayOffDelayActive) {

          relayOffDelayStartTime = currentTime;

          relayOffDelayActive = true;

          Serial.println("Tegangan tinggi terdeteksi. Memulai hitung mundur untuk OFF relay...");

          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).");

            displayNeedsUpdate = true; 

          }

          relayOffDelayActive = false;

        }

      } else {

        if (relayOffDelayActive) { 

            relayOffDelayActive = false;

            Serial.println("Delay OFF relay dibatalkan, tegangan kembali normal.");

            displayNeedsUpdate = true; 

        }

      }


      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 { 

    if (digitalRead(RELAY_PIN) == HIGH) { 

      digitalWrite(RELAY_PIN, 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)");

        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 {

    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;


    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;

    }

  }


  // --- 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) {

    if (!buttonIsCurrentlyPressed) {

      buttonIsCurrentlyPressed = true;

      pushButtonPressTime = currentTime;

    }

  } else {

    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; // Selalu true agar teks selalu terlihat


  display.setTextSize(1); // Ukuran teks 1 (6x8 piksel per karakter)

  display.setTextColor(SSD1306_WHITE); // Warna teks putih


  if (currentMode == MODE_NORMAL) {

    snprintf(currentLine0, sizeof(currentLine0), "Volt Aki: %.2f V", teganganAki); 

    

    const char* relayStatus = (digitalRead(RELAY_PIN) == HIGH) ? "ON" : "OFF";

    snprintf(currentLine1, sizeof(currentLine1), "RELAY: %s", relayStatus);


    snprintf(currentLine2, sizeof(currentLine2), ""); // Kosongkan baris

    snprintf(currentLine3, sizeof(currentLine3), ""); // Kosongkan baris


  } 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(); // Kirim buffer ke layar OLED


    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 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.");

}

CARGER AKI CONTROLER INA226-LIBARY WE ROTARI SWITCH DAN PUS LCD 16X 2 SUKSES NORMAL SELISIH 0.2 VOLT

 #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.");

}

CARGER AKI CONTROLER INA226-LIBARY WE ROTARI SWITCH DAN PUS LCD 16X 2 SUKSES NORMAL

 #include <Wire.h> // Untuk komunikasi I2C

#include <LiquidCrystal_I2C.h> // Library untuk LCD I2C - PASTIKAN INI TERINSTAL!

#include <INA226_WE.h> // Library untuk INA226_WE

#include <Preferences.h> // Untuk menyimpan data di NVS (Non-Volatile Storage) ESP32


// --- DEKLARASI PIN ESP32 ---

// Alamat I2C umum untuk LCD 16x2 dengan modul PCF8574 adalah 0x27 atau 0x3F

// Coba 0x27 terlebih dahulu, jika tidak berfungsi, coba 0x3F.

LiquidCrystal_I2C lcd(0x27, 16, 2); // Alamat I2C, Kolom, Baris


// Pin untuk Rotary Encoder

#define ROTARY_A_PIN 32 // Ganti dengan pin GPIO yang Anda gunakan

#define ROTARY_B_PIN 33 // Ganti dengan pin GPIO yang Anda gunakan

#define ROTARY_SW_PIN 25 // Pin untuk tombol push Rotary Encoder


// Pin untuk Relay

#define RELAY_PIN 26 // Ganti dengan pin GPIO yang terhubung ke modul relay


// --- OBJEK SENSOR ---

INA226_WE ina226; // Membuat objek INA226 menggunakan library INA226_WE


// --- OBJEK NVS ---

Preferences preferences;


// --- PARAMETER AKI & BATAS ---

float batasTeganganBawah = 10.80; // Nilai default, akan dimuat dari NVS (diubah ke 2 desimal)

float batasTeganganAtas = 14.80; // Nilai default, akan dimuat dari NVS (diubah ke 2 desimal)


// --- VARIABEL PENGUKURAN ---

float teganganAki = 0.0;


// --- VARIABEL KONTROL MODE & UI ---

enum ModeAlat {

  MODE_NORMAL,

  MODE_SETTING_SELECT,      // Mode baru: memilih antara batas bawah/atas

  MODE_SETTING_BAWAH_EDIT,  // Mengedit nilai batas bawah

  MODE_SETTING_ATAS_EDIT    // Mengedit nilai batas atas

};

ModeAlat currentMode = MODE_NORMAL;


// Untuk Rotary Encoder

volatile int encoderPos = 0; // Posisi encoder saat ini (volatile karena diakses dari ISR)

int lastEncoderPos = 0;      // Posisi encoder terakhir yang dibaca di loop


// UNTUK DEBOUNCING ROTARY ENCODER YANG LEBIH BAIK (Gray Code)

volatile int lastEncoded = 0; // Menyimpan kombinasi state A dan B sebelumnya

const int ENCODER_DEBOUNCE_DELAY = 1; // Biarkan ini kecil, algoritma Gray Code yang utama


// --- TAMBAHAN BARU UNTUK SENSITIVITAS ROTARY ENCODER ---

// Sesuaikan nilai ini. Jika 1 klik fisik encoder menyebabkan encoderPos berubah 2, set ke 2.

// Jika 1 klik fisik menyebabkan encoderPos berubah 4 (umum), set ke 4.

// Mulai dengan 2 atau 4, lalu coba sesuaikan.

const int ENCODER_STEPS_PER_CLICK = 4; // Asumsi 4 perubahan di encoderPos per satu 'detent'/klik

int encoderClickCounter = 0; // Untuk mengakumulasi langkah-langkah internal encoder


// Untuk Tombol Push (Long Press & Single Press)

volatile unsigned long pushButtonPressTime = 0; // Waktu tombol ditekan (di ISR)

volatile bool buttonIsCurrentlyPressed = false; // Status tombol fisik sedang ditekan (di ISR)


unsigned long lastButtonStateChangeTime = 0; // Waktu terakhir state tombol berubah (di loop)

const long BUTTON_DEBOUNCE_MS = 150; // Debounce delay untuk tombol, misalnya ke 150ms


volatile bool shortPressDetected = false; // Flag untuk short press yang sudah di-debounce

volatile bool longPressDetected = false; // Flag untuk long press yang sudah di-debounce


const long LONG_PRESS_DURATION = 3000; // 3 detik


// Untuk Kedip-kedip Teks di LCD

bool blinkState = false;

unsigned long lastBlinkMillis = 0;

const long BLINK_INTERVAL = 500; // 500 ms


// Variabel untuk mode pemilihan pengaturan

int selectedSetting = 0; // 0 = Batas Bawah, 1 = Batas Atas


// Variabel untuk tampilan pesan "DISIMPAN"

unsigned long messageDisplayStartTime = 0;

const long MESSAGE_DISPLAY_DURATION = 1000; // 1 detik


// --- VARIABEL UNTUK OPTIMALISASI UPDATE LCD ---

char lastLine0[17] = ""; // Untuk menyimpan teks terakhir di baris 0

char lastLine1[17] = ""; // Untuk menyimpan teks terakhir di baris 1

ModeAlat lastMode = MODE_NORMAL; // Untuk menyimpan mode terakhir

int lastSelectedSetting = -1; // Untuk menyimpan selectedSetting terakhir (-1 agar update pertama dipaksa)

float lastTeganganAki = -1.0; // Untuk menyimpan tegangan aki terakhir

bool displayNeedsUpdate = true; // Flag untuk memaksa update di awal atau saat mode berubah


// --- PROTOTYPE FUNGSI ---

// Deklarasi fungsi agar bisa dipanggil sebelum didefinisikan

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();      // Inisialisasi LCD

  lcd.backlight(); // Nyalakan backlight

  lcd.clear();

  lcd.setCursor(0, 0);

  lcd.print("Inisialisasi...");

  Serial.println("Inisialisasi LCD...");

  delay(1000);


  // --- Inisialisasi INA226 ---

  Wire.begin(); // PENTING: Inisialisasi komunikasi I2C sebelum menggunakan sensor

  Wire.setClock(50000); // Mengatur kecepatan I2C ke 50 kHz (default 100 kHz)


  ina226.init(); // Panggil init() untuk memulai inisialisasi sensor


  Serial.println("INA226 ditemukan (asumsi koneksi OK).");


  // KONFIGURASI INA226 (disesuaikan dengan INA226_WE.h yang Anda berikan)

  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); // Tombol push dengan pull-up internal


  // Attach Interrupts untuk Rotary Encoder pada kedua pin A dan B

  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


  // --- Muat Pengaturan dari NVS ---

  loadSettings();

  Serial.print("Batas Bawah: "); Serial.println(batasTeganganBawah, 2); // Tampilkan dengan 2 desimal

  Serial.print("Batas Atas: "); Serial.println(batasTeganganAtas, 2);   // Tampilkan dengan 2 desimal


  // Inisialisasi awal lastEncoded dengan membaca pin saat setup

  int initialEncoderAPinState = digitalRead(ROTARY_A_PIN);

  int initialEncoderBPinState = digitalRead(ROTARY_B_PIN);

  lastEncoded = (initialEncoderAPinState << 1) | initialEncoderBPinState;


  lcd.clear(); // Bersihkan LCD setelah inisialisasi

}


// --- FUNGSI LOOP ---

void loop() {

  unsigned long currentTime = millis(); // Ambil waktu saat ini sekali per loop


  // --- BACA SENSOR ---

  teganganAki = ina226.getBusVoltage_V();

  // Jika tegangan aki berubah secara signifikan, paksa update tampilan

  // Toleransi 0.01V untuk memicu update (karena sekarang kita pakai 2 desimal)

  if (abs(teganganAki - lastTeganganAki) > 0.01) { 

    displayNeedsUpdate = true;

  }


  // Tangani putaran Rotary Encoder

  // Perubahan encoderPos sekarang ditangani oleh ISR readEncoder()

  if (encoderPos != lastEncoderPos) {

    int encoderDelta = encoderPos - lastEncoderPos; // Perubahan encoderPos

    encoderClickCounter += encoderDelta; // Akumulasi perubahan dari ISR


    // Hanya proses jika akumulasi perubahan mencapai ambang batas per "klik" fisik yang diinginkan

    if (abs(encoderClickCounter) >= ENCODER_STEPS_PER_CLICK) {

      int numClicks = encoderClickCounter / ENCODER_STEPS_PER_CLICK; // Berapa "klik" yang terdeteksi

      encoderClickCounter %= ENCODER_STEPS_PER_CLICK; // Sisa putaran jika ada (untuk putaran di tengah detent)


      // Logika penyesuaian nilai berdasarkan "klik" yang terdeteksi

      if (currentMode == MODE_SETTING_BAWAH_EDIT) {

        batasTeganganBawah += (float)numClicks * 0.01; // Ubah per 0.01V

        batasTeganganBawah = round(batasTeganganBawah * 100.0) / 100.0; // Bulatkan ke 2 desimal

        batasTeganganBawah = constrain(batasTeganganBawah, 0.0, 36.0);


        blinkState = true;

        lastBlinkMillis = currentTime;

      } else if (currentMode == MODE_SETTING_ATAS_EDIT) {

        batasTeganganAtas += (float)numClicks * 0.01; // Ubah per 0.01V

        batasTeganganAtas = round(batasTeganganAtas * 100.0) / 100.0; // Bulatkan ke 2 desimal

        batasTeganganAtas = constrain(batasTeganganAtas, 0.0, 36.0);


        blinkState = true;

        lastBlinkMillis = currentTime;

      } else if (currentMode == MODE_SETTING_SELECT) {

        // Untuk pemilihan menu, setiap "klik" terdeteksi berarti 1 langkah menu

        selectedSetting += numClicks;

        if (selectedSetting < 0) selectedSetting = 1; // Wrap around (0 ke 1, 1 ke 0)

        if (selectedSetting > 1) selectedSetting = 0; // Wrap around (1 ke 0, 0 ke 1)

        

        blinkState = true; // Pastikan teks tidak berkedip saat memilih menu

      }

      displayNeedsUpdate = true; // Paksa update LCD saat encoder diproses

    }

    lastEncoderPos = encoderPos; // Selalu perbarui lastEncoderPos di sini, setelah potensial perubahan

  }


  // --- LOGIKA KONTROL RELAY ---

  if (currentMode == MODE_NORMAL) {

    if (digitalRead(RELAY_PIN) == LOW && teganganAki < batasTeganganBawah) {

      digitalWrite(RELAY_PIN, HIGH);

      // Serial.println("Relay ON (Tegangan rendah)"); // Uncomment for debugging

    } else if (digitalRead(RELAY_PIN) == HIGH && teganganAki > batasTeganganAtas) {

      digitalWrite(RELAY_PIN, LOW);

      // Serial.println("Relay OFF (Tegangan tinggi)"); // Uncomment for debugging

    }

  } else {

    digitalWrite(RELAY_PIN, LOW); // Relay OFF saat di mode setting

  }


  // --- LOGIKA PENANGANAN TOMBOL PUSH ROTARY (Sudah ditingkatkan) ---

  if (buttonIsCurrentlyPressed) { // Tombol sedang ditekan

    // Deteksi long press

    if (!longPressDetected && (currentTime - pushButtonPressTime >= LONG_PRESS_DURATION)) {

      longPressDetected = true; // Set flag long press

      shortPressDetected = false; // Pastikan short press tidak terpicu jika ini long press


      // --- AKSI LONG PRESS ---

      if (currentMode == MODE_NORMAL) {

        currentMode = MODE_SETTING_SELECT; // Masuk ke mode pemilihan pengaturan

        Serial.println("Mode: Pilih Pengaturan (Long Press)");

        blinkState = true; // Pastikan teks muncul dan tidak berkedip saat masuk mode pilih

        selectedSetting = 0;

        displayNeedsUpdate = true;

      } else {

        currentMode = MODE_NORMAL;

        saveSettings(); // Simpan pengaturan saat keluar dari mode setting

        Serial.println("Mode: Normal (Long Press Keluar, Pengaturan Disimpan)");

        messageDisplayStartTime = currentTime; // Mulai timer tampilan pesan "DISIMPAN!"

        blinkState = true; // Pastikan teks muncul dan tidak berkedip saat kembali ke mode normal

        displayNeedsUpdate = true;

      }

      lastButtonStateChangeTime = currentTime; // Update waktu perubahan state tombol

    }

  } else { // Tombol tidak ditekan (sudah dilepas)

    // Deteksi short press (jika tombol dilepas SEBELUM durasi long press)

    // dan sudah melewati debounce time sejak dilepas

    if (pushButtonPressTime != 0 && (currentTime - pushButtonPressTime < LONG_PRESS_DURATION) && !longPressDetected) {

      if (currentTime - lastButtonStateChangeTime > BUTTON_DEBOUNCE_MS) { // Debounce untuk pelepasan short press

        shortPressDetected = true; // Set flag short press

        lastButtonStateChangeTime = currentTime; // Update waktu perubahan state tombol

      }

    }

    // Reset semua flag dan waktu setelah tombol dilepas dan logikanya diproses

    pushButtonPressTime = 0; // Reset waktu penekanan

    longPressDetected = false; // Reset flag long press

  }


  // --- TANGANI AKSI SHORT PRESS (SETELAH DEBOUNCE) ---

  if (shortPressDetected) {

    shortPressDetected = false; // Reset flag segera


    if (currentMode == MODE_NORMAL) {

      Serial.println("Short press di Mode Normal. Tidak ada aksi.");

      // Tambahkan aksi lain di sini jika diinginkan

    } 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");

      }

      // Mulai kedip saat masuk mode edit

      blinkState = true; // Pastikan teks muncul saat masuk mode edit

      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");

      // Hentikan kedip saat keluar dari mode edit

      blinkState = true; // Pastikan teks muncul saat kembali ke pemilihan

      displayNeedsUpdate = true;

    }

  }


  // --- UPDATE TAMPILAN LCD ---

  // Tampilkan pesan "DISIMPAN!" jika sedang aktif

  if (messageDisplayStartTime != 0 && (currentTime - messageDisplayStartTime < MESSAGE_DISPLAY_DURATION)) {

    // Pastikan LCD menampilkan pesan "DISIMPAN" tanpa interferensi

    if (strcmp(lastLine0, "PENGATURAN") != 0 || strcmp(lastLine1, "DISIMPAN!") != 0) {

        lcd.setCursor(0, 0); lcd.print("PENGATURAN      "); // Tambah spasi untuk hapus sisa

        lcd.setCursor(0, 1); lcd.print("DISIMPAN!       "); // Tambah spasi untuk hapus sisa

        strcpy(lastLine0, "PENGATURAN");

        strcpy(lastLine1, "DISIMPAN!");

    }

  } else {

    // Jika tidak ada pesan, update tampilan normal (selektif)

    updateLCDDisplay();

    messageDisplayStartTime = 0; // Reset setelah waktu habis

  }


  delay(10);

}


// --- FUNGSI INTERRUPT ROTARY ENCODER (Gray Code Decoder) ---

// ISR ini hanya mengupdate encoderPos berdasarkan pola perubahan pin A & B.

// Debouncing "per klik" dilakukan di loop().

void IRAM_ATTR readEncoder() {

  // Baca state pin A dan B saat ini

  int encoderAPinState = digitalRead(ROTARY_A_PIN);

  int encoderBPinState = digitalRead(ROTARY_B_PIN);


  // Gabungkan state A dan B menjadi satu nilai 2-bit (00, 01, 10, 11)

  int encoded = (encoderAPinState << 1) | encoderBPinState;


  // Bandingkan dengan state sebelumnya untuk mendeteksi putaran

  // Ini adalah logika untuk Gray Code decoder

  if (encoded == 0b00) { // State 00

    if (lastEncoded == 0b01) encoderPos--; // Dari 01 ke 00 (CCW)

    else if (lastEncoded == 0b10) encoderPos++; // Dari 10 ke 00 (CW)

  } else if (encoded == 0b01) { // State 01

    if (lastEncoded == 0b00) encoderPos++; // Dari 00 ke 01 (CW)

    else if (lastEncoded == 0b11) encoderPos--; // Dari 11 ke 01 (CCW)

  } else if (encoded == 0b11) { // State 11

    if (lastEncoded == 0b01) encoderPos++; // Dari 01 ke 11 (CW)

    else if (lastEncoded == 0b10) encoderPos--; // Dari 10 ke 11 (CCW)

  } else if (encoded == 0b10) { // State 10

    if (lastEncoded == 0b11) encoderPos++; // Dari 11 ke 10 (CW)

    else if (lastEncoded == 0b00) encoderPos--; // Dari 00 ke 10 (CCW)

  }

  

  // Perbarui lastEncoded untuk deteksi selanjutnya

  lastEncoded = encoded;

}


// --- FUNGSI INTERRUPT TOMBOL PUSH ROTARY ---

void IRAM_ATTR handleButtonInterrupt() {

  unsigned long currentTime = millis(); // Ambil waktu saat ini di ISR


  // Catat waktu penekanan/pelepasan tombol dan status fisik

  if (digitalRead(ROTARY_SW_PIN) == LOW) { // Tombol ditekan

    if (!buttonIsCurrentlyPressed) {

      buttonIsCurrentlyPressed = true;

      pushButtonPressTime = currentTime; // Catat waktu penekanan

    }

  } else { // Tombol dilepas

    buttonIsCurrentlyPressed = false;

  }

}


// --- FUNGSI UPDATE TAMPILAN LCD ---

void updateLCDDisplay() {

  char currentLine0[17];

  char currentLine1[17];


  // Logika kedip-kedip teks

  // Kedip hanya aktif di mode edit

  if (currentMode == MODE_SETTING_BAWAH_EDIT || currentMode == MODE_SETTING_ATAS_EDIT) {

    if (millis() - lastBlinkMillis > BLINK_INTERVAL) {

      blinkState = !blinkState;

      lastBlinkMillis = millis();

      displayNeedsUpdate = true; // Paksa update jika status kedip berubah

    }

  } else {

    // Pastikan tidak berkedip di mode lain (selalu tampil)

    // Set blinkState ke true agar teks selalu terlihat (tidak berkedip)

    if (!blinkState) {

        blinkState = true;

        displayNeedsUpdate = true; // Paksa update untuk memastikan tampilan tanpa kedip

    }

  }


  // Tentukan konten yang akan ditampilkan berdasarkan currentMode

  // Gunakan snprintf untuk mengisi currentLine0 dan currentLine1

  if (currentMode == MODE_NORMAL) {

    snprintf(currentLine0, sizeof(currentLine0), "Volt Aki: %.1f V", teganganAki); // Tetap 1 desimal untuk pengukuran

    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); // Ubah ke 2 desimal

    } else {

      snprintf(currentLine1, sizeof(currentLine1), "                "); // Tampilan kosong untuk efek kedip

    }

  } else if (currentMode == MODE_SETTING_ATAS_EDIT) {

    snprintf(currentLine0, sizeof(currentLine0), "EDIT BATAS ATAS ");

    if (blinkState) {

      snprintf(currentLine1, sizeof(currentLine1), "%.2f V", batasTeganganAtas); // Ubah ke 2 desimal

    } else {

      snprintf(currentLine1, sizeof(currentLine1), "                "); // Tampilan kosong untuk efek kedip

    }

  }


  // --- HANYA TULIS KE LCD JIKA ADA PERUBAHAN ATAU DIPAKSA UPDATE ---

  // Periksa apakah mode berubah, atau jika konten baris berubah

  if (currentMode != lastMode || displayNeedsUpdate || (strcmp(currentLine0, lastLine0) != 0) || (strcmp(currentLine1, lastLine1) != 0)) {

    // Bersihkan seluruh baris sebelum menulis untuk menghindari sisa karakter

    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(" ");


    // Kemudian tulis teks baru

    lcd.setCursor(0, 0); lcd.print(currentLine0);

    lcd.setCursor(0, 1); lcd.print(currentLine1);


    // Salin konten yang baru ditulis ke variabel lastLine untuk perbandingan berikutnya

    strcpy(lastLine0, currentLine0);

    strcpy(lastLine1, currentLine1);


    // Reset flag paksa update setelah menulis

    displayNeedsUpdate = false;

  }


  // Perbarui state terakhir untuk perbandingan di iterasi loop berikutnya

  lastMode = currentMode;

  lastSelectedSetting = selectedSetting;

  lastTeganganAki = teganganAki;

}


// --- FUNGSI SIMPAN/MUAT PENGATURAN KE NVS (Non-Volatile Storage) ---

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); // Muat dengan 2 desimal

  batasTeganganAtas = preferences.getFloat("batasAtas", 14.80);   // Muat dengan 2 desimal

  preferences.end();

  Serial.println("Pengaturan dimuat.");

}

CARGER AKI CONTROLER INA226-LIBARY WE ROTARI SW+OLED 0.96 SUKSES TAMPILAN LEBIH BAIK

 #include <Wire.h>  #include <Adafruit_GFX.h>    // Library grafis Adafruit #include <Adafruit_SSD1306.h> // Library untuk...