#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