Search This Blog

Monday, 14 July 2025

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

}

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