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

}

No comments:

Post a Comment

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