#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.");
}
No comments:
Post a Comment