CHARGER CONTROLER
berikut adalah ringkasan lengkap mengenai proyek Charger Controller by PUTU yang telah kita kembangkan, mencakup komponen yang digunakan, diagram pengkabelan (wiring diagram), dan library Arduino yang dipakai.
1. Komponen yang Digunakan
Proyek ini menggunakan mikrokontroler ESP32 sebagai otak utama dan beberapa komponen pendukung untuk fungsi pengisian daya dan antarmuka pengguna:
ESP32 Development Board: Mikrokontroler utama yang menjalankan semua logika program, mengelola koneksi WiFi, membaca sensor, mengontrol relay, dan berinteraksi dengan OLED serta rotary encoder.
OLED Display 0.96 inci (SSD1306): Layar kecil monokrom yang digunakan untuk menampilkan informasi real-time seperti voltase aki, status relay, dan menu pengaturan.
INA226 Current/Voltage Sensor Module: Modul sensor presisi tinggi yang digunakan untuk membaca tegangan (voltase) aki. Sensor ini berkomunikasi dengan ESP32 melalui protokol I2C.
Rotary Encoder dengan Push Button: Digunakan sebagai antarmuka pengguna utama untuk menavigasi menu, mengubah nilai batas tegangan, dan mengaktifkan/menonaktifkan mode manual relay.
Pin A dan B untuk mendeteksi putaran.
Pin SW (Switch) untuk mendeteksi penekanan tombol.
Relay Module (1 Channel): Sakelar elektronik yang dikendalikan oleh ESP32 untuk menghidupkan atau mematikan aliran listrik ke charger aki, berdasarkan logika tegangan dan pengaturan yang telah ditentukan.
Kabel Jumper: Untuk menghubungkan semua komponen.
Sumber Daya (Power Supply): Untuk memberi daya pada ESP32 dan modul lainnya (misalnya, kabel USB ke PC atau adaptor 5V).
Aki/Baterai: Aki yang akan diukur dan dikontrol pengisian dayanya.
Charger Aki: Perangkat yang akan dihidupkan/dimatikan oleh relay untuk mengisi aki.
2. Diagram Pengkabelan (Wiring Diagram)
Berikut adalah deskripsi koneksi antara ESP32 dan masing-masing komponen. Penting untuk memastikan koneksi yang benar untuk fungsionalitas yang optimal.
ESP32 ke OLED Display (SSD1306):
OLED VCC->ESP32 3.3Vatau5V(tergantung spesifikasi OLED, 3.3V lebih aman jika OLED mendukung)OLED GND->ESP32 GNDOLED SDA->ESP32 GPIO 21(Data I2C)OLED SCL->ESP32 GPIO 22(Clock I2C)
ESP32 ke INA226 Sensor Module:
INA226 VCC->ESP32 3.3Vatau5VINA226 GND->ESP32 GNDINA226 SDA->ESP32 GPIO 21(Berbagi jalur I2C dengan OLED)INA226 SCL->ESP32 GPIO 22(Berbagi jalur I2C dengan OLED)INA226 VIN+->Positif Aki(melalui shunt resistor INA226)INA226 VIN-->Negatif Aki(melalui shunt resistor INA226)Catatan Penting: Pastikan INA226 terhubung dengan benar untuk mengukur tegangan aki. Biasanya,
VIN+ke sisi positif aki, danVIN-ke sisi negatif aki (atau setelah shunt resistor jika mengukur arus sekaligus).
ESP32 ke Rotary Encoder:
Rotary A Pin->ESP32 GPIO 32(Input dengan internal Pull-up)Rotary B Pin->ESP32 GPIO 33(Input dengan internal Pull-up)Rotary SW Pin->ESP32 GPIO 25(Input dengan internal Pull-up)Rotary VCC->ESP32 3.3Vatau5VRotary GND->ESP32 GND
ESP32 ke Relay Module:
Relay IN(atauSIG/IN1) ->ESP32 GPIO 26(Output)Relay VCC->ESP32 5V(Modul relay seringkali membutuhkan 5V, bahkan jika ESP32 berjalan di 3.3V)Relay GND->ESP32 GNDKoneksi Charger: Terminal
NO(Normally Open) danCOM(Common) pada relay dihubungkan secara seri dengan jalur positif atau negatif dari charger aki ke aki, sehingga relay dapat memutus atau menyambungkan aliran daya charger.
3. Library Arduino yang Digunakan
Proyek ini memanfaatkan beberapa library Arduino untuk mempermudah interaksi dengan komponen hardware dan fungsionalitas jaringan:
Wire.h:Fungsi: Library standar Arduino untuk komunikasi I2C (Inter-Integrated Circuit). Digunakan untuk berkomunikasi dengan OLED Display (SSD1306) dan sensor INA226.
Adafruit_GFX.h:Fungsi: Library grafis inti dari Adafruit yang menyediakan fungsi-fungsi dasar untuk menggambar pada berbagai jenis layar. Ini adalah dependensi untuk
Adafruit_SSD1306.
Adafruit_SSD1306.h:Fungsi: Library khusus untuk mengontrol OLED Display yang berbasis chip SSD1306. Memungkinkan penulisan teks, menggambar bentuk, dll., pada layar OLED.
INA226_WE.h:Fungsi: Library yang mempermudah pembacaan data dari sensor INA226 (tegangan, arus, daya). Library ini mengabstraksi detail komunikasi I2C dengan sensor.
Preferences.h:Fungsi: Library khusus ESP32 untuk menyimpan data secara permanen di NVS (Non-Volatile Storage) flash memory. Digunakan untuk menyimpan pengaturan batas tegangan dan kredensial WiFi, sehingga tidak hilang saat ESP32 mati atau restart.
WiFi.h:Fungsi: Library inti ESP32 untuk mengelola konektivitas WiFi, baik sebagai Station (client) untuk terhubung ke router lain, maupun sebagai Access Point (AP) untuk membuat jaringannya sendiri.
WebServer.h:Fungsi: Library untuk membuat server HTTP sederhana di ESP32. Digunakan untuk menyajikan halaman web, menerima input dari browser (pengaturan batas, kredensial WiFi), dan menyediakan data real-time (voltase, status relay) melalui API.
Dengan kombinasi komponen dan library ini, proyek Charger Controller dapat berfungsi secara mandiri dengan antarmuka fisik (OLED + Rotary Encoder) dan juga dapat dikontrol serta dipantau dari jarak jauh melalui antarmuka web.
BERIKUT KODING NYA
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <INA226_WE.h>
#include <Preferences.h>
#include <WiFi.h>
#include <WebServer.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; // Default untuk aki 12V
float batasTeganganAtas = 14.80; // Default untuk aki 12V
// --- 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_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; // Digunakan jika nanti ada indikator berkedip, saat ini tidak terlalu esensial
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; // 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; // Mengindikasikan apakah tampilan perlu diperbarui
bool lastRelayStateDisplayed = false;
// Variabel untuk mengontrol frekuensi update OLED
unsigned long lastOLEDUpdateTime = 0;
const long OLED_UPDATE_INTERVAL_MS = 200; // Update OLED setiap 200ms
// --- PENGATURAN WIFI DAN WEBSERVER ---
// Variabel untuk menyimpan pengaturan WiFi Client dari NVS
String wifi_ssid = "";
String wifi_password = "";
bool useStaticIP = false;
IPAddress staticIP;
IPAddress staticGateway;
IPAddress staticSubnet;
IPAddress staticDNS;
WebServer server(80);
// NAMA AKSES POIN BARU (Default jika WiFi Client gagal/belum diatur)
const char* ap_ssid = "CHARGER CONTROLER by PUTU";
const char* ap_password = "12345678";
// --- VARIABEL UNTUK DIGIT SELECTION (Rotary Encoder) ---
int currentDigitEdit = 0; // 0: 0.01, 1: 0.1, 2: 1.0
// --- PROTOTYPE FUNGSI ---
// Deklarasi fungsi-fungsi agar compiler tahu mereka ada sebelum digunakan
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
// PROTOTIPE UNTUK FUNGSI WEBSERVER (PENTING UNTUK MENGHILANGKAN ERROR "UNDEFINED REFERENCE")
void handleGetVoltage();
void handleGetRelayStatus();
String webPage();
void handleRoot();
void handleSet();
void handleSaveWifi();
void updateDisplayForWebChanges(); // Fungsi untuk memicu update OLED setelah perubahan dari web
void connectToWiFi(); // Fungsi untuk mencoba koneksi WiFi
// --- 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 (;;); // Loop tak terbatas jika OLED gagal
}
Serial.println("Inisialisasi OLED...");
display.display(); // Tampilkan buffer splash screen
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);
// Attach Interrupts
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 start
lastRelayStateDisplayed = (digitalRead(RELAY_PIN) == HIGH); // Inisialisasi status display relay
// --- Muat Pengaturan dari NVS ---
loadSettings();
Serial.print("Batas Bawah dimuat: "); Serial.println(batasTeganganBawah, 2);
Serial.print("Batas Atas dimuat: "); Serial.println(batasTeganganAtas, 2);
// Inisialisasi awal encoder position
int initialEncoderAPinState = digitalRead(ROTARY_A_PIN);
int initialEncoderBPinState = digitalRead(ROTARY_B_PIN);
lastEncoded = (initialEncoderAPinState << 1) | initialEncoderBPinState;
display.clearDisplay();
displayNeedsUpdate = true; // Paksa update display pertama kali
// --- SETUP WIFI DAN WEBSERVER ---
WiFi.mode(WIFI_AP_STA); // Mengaktifkan mode Station (untuk terhubung ke WiFi rumah) dan Access Point (jika koneksi gagal)
connectToWiFi(); // Coba konek ke WiFi dengan pengaturan yang dimuat
// Setup Web Server Routes
server.on("/", handleRoot);
server.on("/set", HTTP_POST, handleSet);
server.on("/voltage", handleGetVoltage);
server.on("/relay_status", handleGetRelayStatus);
server.on("/save_wifi", HTTP_POST, handleSaveWifi);
server.begin();
Serial.println("Web server berjalan di port 80");
}
// --- FUNGSI LOOP ---
void loop() {
unsigned long currentTime = millis();
// Handle request dari Web Server
server.handleClient();
// --- BACA SENSOR ---
teganganAki = ina226.getBusVoltage_V();
// 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;
float increment = 0.0;
// Perubahan: Implementasi currentDigitEdit untuk inkremen
if (currentDigitEdit == 0) { // 0.01
increment = 0.01;
} else if (currentDigitEdit == 1) { // 0.1
increment = 0.1;
} else if (currentDigitEdit == 2) { // 1.0
increment = 1.0;
}
if (currentMode == MODE_SETTING_BAWAH_EDIT) {
batasTeganganBawah += (float)numClicks * increment; // Menggunakan increment yang dipilih
batasTeganganBawah = round(batasTeganganBawah * 100.0) / 100.0;
batasTeganganBawah = constrain(batasTeganganBawah, 0.0, 36.0);
displayNeedsUpdate = true; // Perubahan nilai memicu update
} else if (currentMode == MODE_SETTING_ATAS_EDIT) {
batasTeganganAtas += (float)numClicks * increment; // Menggunakan increment yang dipilih
batasTeganganAtas = round(batasTeganganAtas * 100.0) / 100.0;
batasTeganganAtas = constrain(batasTeganganAtas, 0.0, 36.0);
displayNeedsUpdate = true; // Perubahan nilai memicu update
} else if (currentMode == MODE_SETTING_SELECT) {
selectedSetting += numClicks;
if (selectedSetting < 0) selectedSetting = 1;
if (selectedSetting > 1) selectedSetting = 0;
displayNeedsUpdate = true; // Perubahan seleksi memicu update
}
}
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...");
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
}
} 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;
}
}
}
// 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; // Pastikan short press tidak terpicu
// Aksi saat long press
if (currentMode == MODE_NORMAL) {
currentMode = MODE_SETTING_SELECT;
Serial.println("Mode: Pilih Pengaturan (Long Press)");
selectedSetting = 0;
displayNeedsUpdate = true;
} else { // Jika berada di mode pengaturan, long press akan keluar dan menyimpan
currentMode = MODE_NORMAL;
saveSettings(); // Simpan pengaturan saat keluar dari mode setting
Serial.println("Mode: Normal (Long Press Keluar, Pengaturan Disimpan)");
messageDisplayStartTime = currentTime; // Tampilkan pesan "Pengaturan Disimpan"
displayNeedsUpdate = true;
currentDigitEdit = 0; // Reset digit edit saat keluar dari mode setting
}
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; // Reset pushButtonPressTime setelah tombol dilepas
longPressDetected = false; // Reset longPressDetected
}
// --- 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");
}
currentDigitEdit = 0; // Reset digit edit ke 0.01 saat masuk mode edit
displayNeedsUpdate = true;
} else if (currentMode == MODE_SETTING_BAWAH_EDIT || currentMode == MODE_SETTING_ATAS_EDIT) {
// Perubahan: Short press akan mengganti digit aktif
currentDigitEdit = (currentDigitEdit + 1) % 3; // 0 -> 1 -> 2 -> 0
Serial.print("Mengubah digit edit ke: ");
if(currentDigitEdit == 0) Serial.println("0.01");
else if(currentDigitEdit == 1) Serial.println("0.1");
else Serial.println("1.0");
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 {
// Perubahan: Pastikan update terjadi pada interval atau jika ada perubahan
if (currentTime - lastOLEDUpdateTime >= OLED_UPDATE_INTERVAL_MS || displayNeedsUpdate) {
updateOLEDDisplay();
lastOLEDUpdateTime = currentTime;
}
}
delay(10); // Sedikit delay untuk stabilitas
}
// --- 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;
// Logika pembacaan encoder (tetap sama)
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();
// Debounce untuk tombol push
// Ini lebih efektif di interrupt
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() {
char currentLine0[25];
char currentLine1[25];
char currentLine2[25];
char currentLine3[25];
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)"; // (M)anual, (A)utomatis
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), ""); // Baris kosong
if (selectedSetting == 0) { // Pilih Batas Bawah
snprintf(currentLine2, sizeof(currentLine2), "> Batas Bawah");
snprintf(currentLine3, sizeof(currentLine3), " Batas Atas");
} else { // Pilih Batas Atas
snprintf(currentLine2, sizeof(currentLine2), " Batas Bawah");
snprintf(currentLine3, sizeof(currentLine3), "> Batas Atas");
}
} else if (currentMode == MODE_SETTING_BAWAH_EDIT) {
snprintf(currentLine0, sizeof(currentLine0), "EDIT BWH (%.2fV)", batasTeganganBawah);
snprintf(currentLine1, sizeof(currentLine1), ""); // Baris kosong
char valueStr[10];
dtostrf(batasTeganganBawah, 5, 2, valueStr); // Format float ke string
snprintf(currentLine2, sizeof(currentLine2), " %s V", valueStr); // Menambahkan spasi di depan
// Ditambahkan: Indikator digit aktif untuk edit
if (currentDigitEdit == 0) snprintf(currentLine3, sizeof(currentLine3), " ^0.01^"); // 2 spasi dari awal, 3 dari depan
else if (currentDigitEdit == 1) snprintf(currentLine3, sizeof(currentLine3), " ^0.1^"); // 2 spasi dari awal, 2 dari depan
else snprintf(currentLine3, sizeof(currentLine3), " ^1.0^"); // 2 spasi dari awal, 1 dari depan
} else if (currentMode == MODE_SETTING_ATAS_EDIT) {
snprintf(currentLine0, sizeof(currentLine0), "EDIT ATS (%.2fV)", batasTeganganAtas);
snprintf(currentLine1, sizeof(currentLine1), ""); // Baris kosong
char valueStr[10];
dtostrf(batasTeganganAtas, 5, 2, valueStr); // Format float ke string
snprintf(currentLine2, sizeof(currentLine2), " %s V", valueStr); // Menambahkan spasi di depan
// Ditambahkan: Indikator digit aktif untuk edit
if (currentDigitEdit == 0) snprintf(currentLine3, sizeof(currentLine3), " ^0.01^");
else if (currentDigitEdit == 1) snprintf(currentLine3, sizeof(currentLine3), " ^0.1^");
else snprintf(currentLine3, sizeof(currentLine3), " ^1.0^");
}
// --- 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 diperbarui karena ada perubahan.");
display.clearDisplay();
display.setCursor(0, 0);
display.print(currentLine0);
display.setCursor(0, 16);
display.print(currentLine1);
if (SCREEN_HEIGHT >= 64) { // Pastikan layar cukup tinggi untuk 4 baris
display.setCursor(0, 32);
display.print(currentLine2);
display.setCursor(0, 48);
display.print(currentLine3);
}
display.display(); // Tampilkan perubahan ke OLED
// Simpan status display terakhir untuk optimasi
strcpy(lastLine0, currentLine0);
strcpy(lastLine1, currentLine1);
strcpy(lastLine2, currentLine2);
strcpy(lastLine3, currentLine3);
displayNeedsUpdate = false; // Reset flag setelah display diperbarui
} else {
// Serial.println(" OLED TIDAK diperbarui (tidak ada perubahan signifikan terdeteksi).");
}
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); // Pusatkan vertikal
display.print(line1);
display.setCursor(0, (SCREEN_HEIGHT - 16) / 2 + 16); // Baris kedua
display.print(line2);
display.display();
messageDisplayStartTime = millis(); // Mulai timer untuk durasi pesan
}
// --- FUNGSI SIMPAN/MUAT PENGATURAN KE NVS ---
void saveSettings() {
preferences.begin("charger_app", false); // Buka NVS dalam mode read-write
preferences.putFloat("batasBawah", batasTeganganBawah);
preferences.putFloat("batasAtas", batasTeganganAtas);
// Penyimpanan pengaturan WiFi
preferences.putString("wifi_ssid", wifi_ssid);
preferences.putString("wifi_pass", wifi_password);
preferences.putBool("use_static_ip", useStaticIP);
if (useStaticIP) {
preferences.putUInt("static_ip", staticIP);
preferences.putUInt("static_gw", staticGateway);
preferences.putUInt("static_sn", staticSubnet);
preferences.putUInt("static_dns", staticDNS);
}
preferences.end(); // Tutup NVS
Serial.println("Pengaturan disimpan.");
}
void loadSettings() {
preferences.begin("charger_app", true); // Buka NVS dalam mode read-only
batasTeganganBawah = preferences.getFloat("batasBawah", 10.80); // Menggunakan default jika tidak ada
batasTeganganAtas = preferences.getFloat("batasAtas", 14.80); // Menggunakan default jika tidak ada
// Pemuatan pengaturan WiFi
wifi_ssid = preferences.getString("wifi_ssid", "");
wifi_password = preferences.getString("wifi_pass", "");
useStaticIP = preferences.getBool("use_static_ip", false);
if (useStaticIP) {
staticIP = IPAddress(preferences.getUInt("static_ip", 0));
staticGateway = IPAddress(preferences.getUInt("static_gw", 0));
staticSubnet = IPAddress(preferences.getUInt("static_sn", 0));
staticDNS = IPAddress(preferences.getUInt("static_dns", 0));
}
preferences.end(); // Tutup NVS
Serial.println("Pengaturan dimuat.");
}
// --- FUNGSI KONEKSI WIFI ---
void connectToWiFi() {
Serial.println("Mencoba konek ke WiFi client...");
if (wifi_ssid.length() > 0) { // Hanya coba konek jika SSID sudah diatur
Serial.print("SSID: "); Serial.println(wifi_ssid);
if (useStaticIP) {
if (WiFi.config(staticIP, staticGateway, staticSubnet, staticDNS)) {
Serial.println("IP statis diatur.");
} else {
Serial.println("Gagal mengatur IP statis.");
}
} else {
Serial.println("Menggunakan DHCP.");
}
WiFi.begin(wifi_ssid.c_str(), wifi_password.c_str());
unsigned long wifiStart = millis();
while (WiFi.status() != WL_CONNECTED && millis() - wifiStart < 15000) { // Coba 15 detik untuk koneksi
delay(500);
Serial.print(".");
}
if (WiFi.status() == WL_CONNECTED) {
Serial.print("\nTerhubung ke WiFi: ");
Serial.println(WiFi.localIP());
showTempMessage("WiFi Connected", WiFi.localIP().toString().c_str());
return; // Berhasil terhubung, keluar dari fungsi
}
}
// Jika gagal terhubung ke WiFi client atau belum dikonfigurasi, aktifkan AP mode
Serial.println("\nGagal konek ke WiFi client atau belum dikonfigurasi. Mengaktifkan AP mode.");
WiFi.softAP(ap_ssid, ap_password);
showTempMessage("AP Mode Aktif", WiFi.softAPIP().toString().c_str());
}
// --- FUNGSI WEBSERVER ---
// Endpoint API untuk mendapatkan nilai tegangan aki
void handleGetVoltage() {
String voltageString = String(teganganAki, 2); // Format 2 angka di belakang koma
server.send(200, "text/plain", voltageString);
}
// Endpoint API untuk mendapatkan status relay (0=OFF, 1=ON)
void handleGetRelayStatus() {
server.send(200, "text/plain", String(digitalRead(RELAY_PIN)));
}
String webPage() {
String html = "<!DOCTYPE html><html><head><meta name='viewport' content='width=device-width, initial-scale=1'>";
html += "<title>CHARGER CONTROLER by PUTU</title>";
html += "<style>";
html += "body{font-family: sans-serif; text-align: center; margin-top: 20px; background-color: #f4f4f4;}";
html += "input[type=text], input[type=password], input[type=number]{width: 80%; padding: 10px; margin: 8px 0; display: inline-block; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box;}";
html += "input[type=submit], button{width: 80%; background-color: #4CAF50; color: white; padding: 14px 20px; margin: 8px 0; border: none; border-radius: 4px; cursor: pointer;}";
html += "input[type=submit]:hover, button:hover{background-color: #45a049;}";
html += ".container{padding: 16px; border: 1px solid #ccc; border-radius: 8px; width: 320px; margin: 20px auto; background-color: white; box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2); transition: 0.3s;}";
html += ".section-title{margin-top: 30px; margin-bottom: 10px; color: #333;}";
html += "#voltageDisplay{font-size: 2em; margin: 20px 0 10px 0; color: #333; font-weight: bold;}";
html += "#relayStatusDisplay{font-size: 1.5em; margin: 0 0 20px 0; color: #007bff; font-weight: bold;}";
html += ".ip-input-group label{display: block; text-align: left; margin-top: 10px; margin-left: 10%; font-size: 0.9em;}";
html += ".ip-input-group input{width: 78%;}";
html += "#staticIpFields{display: " + String(useStaticIP ? "block" : "none") + ";}";
html += "</style>";
html += "</head><body>";
html += "<div class='container'>";
html += "<h2>CHARGER CONTROLER by PUTU</h2>";
html += "<div id='voltageDisplay'>Loading Voltage...</div>";
html += "<div id='relayStatusDisplay'>Relay: Loading...</div>";
html += "<h3 class='section-title'>Pengaturan Batas Aki</h3>";
html += "<form action='/set' method='post'>";
html += "Batas Bawah (V): <input type='number' step='0.01' name='bawah' value='" + String(batasTeganganBawah, 2) + "'><br>";
html += "Batas Atas (V): <input type='number' step='0.01' name='atas' value='" + String(batasTeganganAtas, 2) + "'><br><br>";
html += "<input type='submit' value='Simpan Batas'>";
html += "</form>";
html += "<h3 class='section-title'>Pengaturan WiFi Client</h3>";
html += "<form action='/save_wifi' method='post'>";
html += "SSID: <input type='text' name='ssid' value='" + wifi_ssid + "'><br>";
html += "Password: <input type='password' name='password' value='" + wifi_password + "'><br>";
html += "<label style='display:block; text-align:left; margin-left:10%; margin-top:15px; margin-bottom:10px;'>";
html += "<input type='checkbox' name='use_static_ip' id='useStaticIPCheckbox' onchange='toggleStaticIPFields()' " + String(useStaticIP ? "checked" : "") + "> Gunakan IP Statis</label>";
html += "<div id='staticIpFields' class='ip-input-group'>";
html += " <label for='ip'>Alamat IP:</label><input type='text' id='ip' name='ip' value='" + staticIP.toString() + "' placeholder='Contoh: 192.168.1.100'><br>";
html += " <label for='gateway'>Gateway:</label><input type='text' id='gateway' name='gateway' value='" + staticGateway.toString() + "' placeholder='Contoh: 192.168.1.1'><br>";
html += " <label for='subnet'>Subnet Mask:</label><input type='text' id='subnet' name='subnet' value='" + staticSubnet.toString() + "' placeholder='Contoh: 255.255.255.0'><br>";
html += " <label for='dns'>DNS (opsional):</label><input type='text' id='dns' name='dns' value='" + staticDNS.toString() + "' placeholder='Contoh: 8.8.8.8'><br>";
html += "</div>";
html += "<input type='submit' value='Simpan WiFi & Restart'>";
html += "</form>";
html += "</div>"; // End container
// --- JavaScript untuk update voltase & relay real-time ---
html += "<script>";
html += "function fetchVoltage() {";
html += " var xhr = new XMLHttpRequest();";
html += " xhr.onreadystatechange = function() {";
html += " if (this.readyState == 4 && this.status == 200) {";
html += " document.getElementById('voltageDisplay').innerHTML = 'Voltase Aki: <b>' + parseFloat(this.responseText).toFixed(2) + ' V</b>';";
html += " }";
html += " };";
html += " xhr.open('GET', '/voltage', true);";
html += " xhr.send();";
html += "}";
html += "setInterval(fetchVoltage, 2000);";
html += "fetchVoltage();";
html += "function fetchRelayStatus() {";
html += " var xhr = new XMLHttpRequest();";
html += " xhr.onreadystatechange = function() {";
html += " if (this.readyState == 4 && this.status == 200) {";
html += " var statusText = this.responseText == '1' ? 'ON' : 'OFF';";
html += " document.getElementById('relayStatusDisplay').innerHTML = 'Relay: <b>' + statusText + '</b>';";
html += " }";
html += " };";
html += " xhr.open('GET', '/relay_status', true);";
html += " xhr.send();";
html += "}";
html += "setInterval(fetchRelayStatus, 2000);";
html += "fetchRelayStatus();";
html += "function toggleStaticIPFields() {";
html += " var checkbox = document.getElementById('useStaticIPCheckbox');";
html += " var staticIpFields = document.getElementById('staticIpFields');";
html += " if (checkbox.checked) {";
html += " staticIpFields.style.display = 'block';";
html += " } else {";
html += " staticIpFields.style.display = 'none';";
html += " }";
html += "}";
html += "toggleStaticIPFields();";
html += "</script>";
html += "</body></html>";
return html;
}
void handleRoot() {
server.send(200, "text/html", webPage());
}
void handleSet() {
if (server.hasArg("bawah") && server.hasArg("atas")) {
float bawah = server.arg("bawah").toFloat();
float atas = server.arg("atas").toFloat();
// Validasi input
if (bawah >= 0 && bawah <= 36 && atas >= 0 && atas <= 36 && bawah < atas) {
batasTeganganBawah = round(bawah * 100.0) / 100.0;
batasTeganganAtas = round(atas * 100.0) / 100.0;
saveSettings(); // Simpan ke NVS
server.send(200, "text/html", "<html><body><h3>Pengaturan Batas Disimpan!</h3><p>Batas Bawah: " + String(batasTeganganBawah, 2) + "V</p><p>Batas Atas: " + String(batasTeganganAtas, 2) + "V</p><a href='/'>Kembali</a></body></html>");
showTempMessage("Pengaturan", "Disimpan via Web!"); // Tampilkan pesan di OLED
displayNeedsUpdate = true; // Set flag untuk update OLED
return;
}
}
server.send(400, "text/html", "<html><body><h3>Input tidak valid!</h3><p>Pastikan Batas Bawah < Batas Atas dan nilai antara 0-36V.</p><a href='/'>Kembali</a></body></html>");
}
// Fungsi untuk menyimpan pengaturan WiFi dari Web UI
void handleSaveWifi() {
Serial.println("Menerima pengaturan WiFi dari web...");
wifi_ssid = server.arg("ssid");
wifi_password = server.arg("password");
useStaticIP = server.hasArg("use_static_ip"); // Cek apakah checkbox dicentang
if (useStaticIP) {
staticIP.fromString(server.arg("ip"));
staticGateway.fromString(server.arg("gateway"));
staticSubnet.fromString(server.arg("subnet"));
staticDNS.fromString(server.arg("dns"));
// Validasi dasar IP
if (staticIP[0] == 0 || staticGateway[0] == 0 || staticSubnet[0] == 0) {
server.send(400, "text/html", "<html><body><h3>Gagal!</h3><p>Pastikan semua alamat IP statis (IP, Gateway, Subnet) terisi dengan benar.</p><a href='/'>Kembali</a></body></html>");
return; // Penting: keluar dari fungsi jika validasi gagal
}
} else {
// Jika tidak menggunakan IP statis, reset nilai IP statis di memori
staticIP = INADDR_NONE;
staticGateway = INADDR_NONE;
staticSubnet = INADDR_NONE;
staticDNS = INADDR_NONE;
}
saveSettings(); // Simpan pengaturan WiFi yang baru ke NVS
server.send(200, "text/html", "<html><body><h3>Pengaturan WiFi Disimpan!</h3><p>ESP32 akan mencoba terhubung kembali.</p><p>SSID: <b>" + wifi_ssid + "</b></p><p>IP Statis: <b>" + (useStaticIP ? staticIP.toString() : "DHCP") + "</b></p><p>Silakan tunggu atau <a href='/'>Kembali</a></p></body></html>");
Serial.println("Pengaturan WiFi disimpan. Restarting...");
showTempMessage("WiFi Diatur", "Restarting...");
delay(2000); // Beri waktu user membaca pesan sebelum restart
ESP.restart(); // Restart ESP32 agar pengaturan WiFi yang baru diterapkan
}
// Fungsi ini dapat dipanggil jika ada perubahan yang memerlukan pembaruan segera di OLED dari logika web
void updateDisplayForWebChanges() {
displayNeedsUpdate = true;
}
Comments
Post a Comment