#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.8; // Nilai default, akan dimuat dari NVS
float batasTeganganAtas = 14.8; // Nilai default, akan dimuat dari NVS
// --- 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 Tombol Push (Long Press & Single Press)
volatile unsigned long pushButtonPressTime = 0; // volatile karena diakses dari ISR
volatile bool buttonPressed = false; // volatile karena diakses dari ISR
volatile bool longPressTriggered = false; // volatile karena diakses dari ISR
volatile bool shortPressFlag = false; // volatile: Flag untuk short press yang dipicu dari ISR
const long LONG_PRESS_DURATION = 3000; // 3 detik
const long DEBOUNCE_DELAY = 50; // Millidetik untuk debounce tombol
// 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
// --- PROTOTYPE FUNGSI ---
// Deklarasi fungsi agar bisa dipanggil sebelum didefinisikan
void IRAM_ATTR readEncoder();
void IRAM_ATTR handleButtonInterrupt();
void saveSettings();
void loadSettings();
void updateLCDDisplay();
void checkLongPress();
// --- 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
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
attachInterrupt(digitalPinToInterrupt(ROTARY_A_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);
Serial.print("Batas Atas: "); Serial.println(batasTeganganAtas);
lcd.clear(); // Bersihkan LCD setelah inisialisasi
}
// --- FUNGSI LOOP ---
void loop() {
// --- BACA SENSOR & INPUT ---
teganganAki = ina226.getBusVoltage_V();
// Tangani putaran Rotary Encoder
if (encoderPos != lastEncoderPos) {
if (currentMode == MODE_SETTING_BAWAH_EDIT) {
batasTeganganBawah += (encoderPos > lastEncoderPos) ? 0.1 : -0.1;
batasTeganganBawah = constrain(batasTeganganBawah, 0.0, 36.0); // Pastikan nilai dalam batas
} else if (currentMode == MODE_SETTING_ATAS_EDIT) {
batasTeganganAtas += (encoderPos > lastEncoderPos) ? 0.1 : -0.1;
batasTeganganAtas = constrain(batasTeganganAtas, 0.0, 36.0); // Pastikan nilai dalam batas
} else if (currentMode == MODE_SETTING_SELECT) {
selectedSetting += (encoderPos > lastEncoderPos) ? 1 : -1;
if (selectedSetting < 0) selectedSetting = 1; // Wrap around
if (selectedSetting > 1) selectedSetting = 0; // Wrap around
}
lastEncoderPos = encoderPos;
blinkState = true; // Reset blink state untuk memastikan tampilan nilai
lastBlinkMillis = millis();
}
// --- 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
}
// Panggil checkLongPress() secara berkala di sini
// checkLongPress() akan menangani aksi long press dan mengatur ulang buttonPressed
checkLongPress();
// --- TANGANI AKSI SHORT PRESS (DIPINDAHKAN DARI ISR) ---
if (shortPressFlag) { // Jika ada flag short press dari ISR
shortPressFlag = 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, misal: toggle backlight, display info lain
} 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");
}
blinkState = true;
lastBlinkMillis = millis();
lcd.clear(); // Clear LCD saat ganti mode
} else if (currentMode == MODE_SETTING_BAWAH_EDIT || currentMode == MODE_SETTING_ATAS_EDIT) {
currentMode = MODE_SETTING_SELECT;
Serial.println("Mode: Kembali ke Pilih Pengaturan");
blinkState = false; // Berhenti berkedip saat kembali ke pemilihan
lcd.clear(); // Clear LCD saat ganti mode
}
}
// --- UPDATE TAMPILAN LCD ---
// Tampilkan pesan "DISIMPAN!" jika sedang aktif
if (messageDisplayStartTime != 0 && (millis() - messageDisplayStartTime < MESSAGE_DISPLAY_DURATION)) {
lcd.setCursor(0, 0); lcd.print("PENGATURAN "); // Tambah spasi untuk hapus sisa
lcd.setCursor(0, 1); lcd.print("DISIMPAN! "); // Tambah spasi untuk hapus sisa
} else {
// Jika tidak ada pesan, update tampilan normal
updateLCDDisplay();
messageDisplayStartTime = 0; // Reset setelah waktu habis
}
delay(10);
}
// --- FUNGSI INTERRUPT ROTARY ENCODER ---
void IRAM_ATTR readEncoder() {
// Teknik ini lebih kuat untuk membaca encoder kuadratur
// Membaca pin B dan membandingkannya dengan state pin A
// ketika pin A berubah.
if (digitalRead(ROTARY_A_PIN) == digitalRead(ROTARY_B_PIN)) {
encoderPos++; // Putar Searah Jarum Jam (CW)
} else {
encoderPos--; // Putar Berlawanan Arah Jarum Jam (CCW)
}
}
// --- FUNGSI INTERRUPT TOMBOL PUSH ROTARY ---
void IRAM_ATTR handleButtonInterrupt() {
static unsigned long lastInterruptTime = 0;
unsigned long currentTime = millis();
// Debounce logic
if (currentTime - lastInterruptTime < DEBOUNCE_DELAY) {
return; // Abaikan jika terlalu cepat setelah interupsi terakhir
}
lastInterruptTime = currentTime; // Perbarui waktu interupsi terakhir
// Hanya set flag di ISR, hindari logika kompleks atau I/O
if (digitalRead(ROTARY_SW_PIN) == LOW) { // Tombol ditekan
if (!buttonPressed) { // Hanya jika sebelumnya tidak dalam keadaan ditekan
buttonPressed = true; // Set flag bahwa tombol sedang ditekan
pushButtonPressTime = currentTime; // Catat waktu penekanan
longPressTriggered = false; // Reset flag long press untuk penekanan baru
}
} else { // Tombol dilepas
if (buttonPressed) { // Hanya jika sebelumnya dalam keadaan ditekan
buttonPressed = false; // Set flag bahwa tombol sudah tidak ditekan
// Jika ini pelepasan tombol dan BUKAN long press yang sudah terpicu sebelumnya
if (!longPressTriggered) {
shortPressFlag = true; // Set flag untuk short press, akan ditangani di loop()
}
}
}
}
// --- FUNGSI UNTUK CEK LONG PRESS (Dipanggil di loop utama) ---
void checkLongPress() {
// Hanya cek jika tombol sedang ditekan dan long press belum terpicu
if (buttonPressed && !longPressTriggered) {
if (millis() - pushButtonPressTime >= LONG_PRESS_DURATION) {
// Long press terdeteksi!
longPressTriggered = true; // Set flag agar aksi ini tidak terulang untuk penekanan yang sama
buttonPressed = false; // PENTING: Reset buttonPressed di sini setelah long press terpicu
// Ini mencegah handleButtonInterrupt() memproses pelepasan tombol ini sebagai short press
if (currentMode == MODE_NORMAL) {
currentMode = MODE_SETTING_SELECT; // Masuk ke mode pemilihan pengaturan
Serial.println("Mode: Pilih Pengaturan (Long Press)");
lcd.clear(); // Bersihkan LCD
blinkState = false; // Pastikan tidak berkedip saat pemilihan
selectedSetting = 0; // Atur default pilihan ke Batas Bawah
} else {
// Jika sudah di mode pengaturan (SELECT, BAWAH_EDIT, atau ATAS_EDIT), long press keluar dan simpan
currentMode = MODE_NORMAL;
saveSettings(); // Simpan pengaturan ke NVS
Serial.println("Mode: Normal (Long Press Keluar, Pengaturan Disimpan)");
messageDisplayStartTime = millis(); // Mulai timer tampilan pesan "DISIMPAN!"
// LCD akan diupdate oleh bagian updateLCDDisplay() di loop utama
}
}
}
}
// --- FUNGSI UPDATE TAMPILAN LCD ---
void updateLCDDisplay() {
// 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();
}
} else {
// Pastikan tidak berkedip di mode lain (selalu tampil)
blinkState = true;
}
char line0[17]; // 16 karakter + null terminator
char line1[17];
// Selalu bersihkan seluruh baris sebelum menulis untuk menghindari sisa karakter
// Ini lebih aman daripada menggunakan spasi di snprintf
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(" ");
if (currentMode == MODE_NORMAL) {
snprintf(line0, sizeof(line0), "Volt Aki: %.1f V", teganganAki);
lcd.setCursor(0, 0); lcd.print(line0);
if (digitalRead(RELAY_PIN) == HIGH) {
snprintf(line1, sizeof(line1), "RELAY: ON");
} else {
snprintf(line1, sizeof(line1), "RELAY: OFF");
}
lcd.setCursor(0, 1); lcd.print(line1);
} else if (currentMode == MODE_SETTING_SELECT) {
lcd.setCursor(0, 0); lcd.print("PILIH PENGATURAN");
if (selectedSetting == 0) {
lcd.setCursor(0, 1); lcd.print("> Batas Bawah");
} else {
lcd.setCursor(0, 1); lcd.print("> Batas Atas");
}
} else if (currentMode == MODE_SETTING_BAWAH_EDIT) {
lcd.setCursor(0, 0); lcd.print("EDIT BATAS BAWAH");
lcd.setCursor(0, 1);
if (blinkState) {
snprintf(line1, sizeof(line1), "%.1f V", batasTeganganBawah);
lcd.print(line1);
} else {
lcd.print(""); // Tampilan kosong untuk efek kedip
}
} else if (currentMode == MODE_SETTING_ATAS_EDIT) {
lcd.setCursor(0, 0); lcd.print("EDIT BATAS ATAS ");
lcd.setCursor(0, 1);
if (blinkState) {
snprintf(line1, sizeof(line1), "%.1f V", batasTeganganAtas);
lcd.print(line1);
} else {
lcd.print(""); // Tampilan kosong untuk efek kedip
}
}
}
// --- 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.8);
batasTeganganAtas = preferences.getFloat("batasAtas", 14.8);
preferences.end();
Serial.println("Pengaturan dimuat.");
}
No comments:
Post a Comment