#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;
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; // 5 menit untuk manual ON
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;
// --- 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;
unsigned long lastBlinkMillis = 0;
const long BLINK_INTERVAL = 500;
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; // DIUBAH DARI 1000 MENJADI 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;
// --- 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);
const char* ap_ssid = "ESP32-Charger"; // SSID untuk Access Point
const char* ap_password = "12345678"; // Password untuk Access Point
// --- VARIABEL UNTUK DIGIT SELECTION ---
int currentDigitEdit = 0; // 0: 0.01, 1: 0.1, 2: 1.0
// --- PROTOTYPE FUNGSI ---
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
// Fungsi untuk Web Server
String webPage();
void handleRoot();
void handleSet();
void handleGetVoltage(); // Fungsi baru untuk endpoint API voltase
void handleSaveWifi(); // Fungsi baru untuk menyimpan pengaturan WiFi
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 (;;);
}
Serial.println("Inisialisasi OLED...");
display.display();
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);
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();
displayNeedsUpdate = true;
// --- 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
server.on("/", handleRoot);
server.on("/set", HTTP_POST, handleSet);
server.on("/voltage", handleGetVoltage); // Endpoint baru untuk mendapatkan voltase
server.on("/save_wifi", HTTP_POST, handleSaveWifi); // Endpoint baru untuk menyimpan pengaturan WiFi
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;
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;
batasTeganganBawah = round(batasTeganganBawah * 100.0) / 100.0;
batasTeganganBawah = constrain(batasTeganganBawah, 0.0, 36.0);
} else if (currentMode == MODE_SETTING_ATAS_EDIT) {
batasTeganganAtas += (float)numClicks * increment;
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 ---
// 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;
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) {
// Jika berada di mode edit, 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 {
if (currentTime - lastOLEDUpdateTime >= OLED_UPDATE_INTERVAL_MS || displayNeedsUpdate) {
updateOLEDDisplay();
lastOLEDUpdateTime = currentTime;
}
}
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) { // 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];
blinkState = true;
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)";
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), "");
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 BWH (%.2fV)", batasTeganganBawah);
snprintf(currentLine1, sizeof(currentLine1), "");
char valueStr[10];
dtostrf(batasTeganganBawah, 5, 2, valueStr); // Format float ke string
snprintf(currentLine2, sizeof(currentLine2), " %s V", valueStr); // Menambahkan spasi di depan
// Tambahkan indikator digit aktif
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), "");
char valueStr[10];
dtostrf(batasTeganganAtas, 5, 2, valueStr); // Format float ke string
snprintf(currentLine2, sizeof(currentLine2), " %s V", valueStr); // Menambahkan spasi di depan
// Tambahkan indikator digit aktif
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) {
display.setCursor(0, 32);
display.print(currentLine2);
display.setCursor(0, 48);
display.print(currentLine3);
}
display.display();
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);
display.print(line1);
display.setCursor(0, (SCREEN_HEIGHT - 16) / 2 + 16);
display.print(line2);
display.display();
messageDisplayStartTime = millis();
}
// --- FUNGSI SIMPAN/MUAT PENGATURAN KE NVS ---
void saveSettings() {
preferences.begin("charger_app", false);
preferences.putFloat("batasBawah", batasTeganganBawah);
preferences.putFloat("batasAtas", batasTeganganAtas);
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();
Serial.println("Pengaturan disimpan.");
}
void loadSettings() {
preferences.begin("charger_app", true);
batasTeganganBawah = preferences.getFloat("batasBawah", 10.80); // Menggunakan default dari kode asli
batasTeganganAtas = preferences.getFloat("batasAtas", 14.80); // Menggunakan default dari kode asli
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();
Serial.println("Pengaturan dimuat.");
}
// --- FUNGSI KONEKSI WIFI ---
void connectToWiFi() {
Serial.println("Mencoba konek ke WiFi client...");
if (wifi_ssid.length() > 0) {
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
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;
}
}
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 ---
String webPage() {
String html = "<!DOCTYPE html><html><head><meta name='viewport' content='width=device-width, initial-scale=1'>";
html += "<title>Pengaturan Charger ESP32</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; color: #333; 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") + ";}"; // Kontrol tampilan IP statis
html += "</style>";
html += "</head><body>";
html += "<div class='container'>";
html += "<h2>Pengaturan Charger ESP32</h2>";
html += "<div id='voltageDisplay'>Loading Voltage...</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>";
// Checkbox untuk IP Statis
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>";
// Kolom input IP Statis (disembunyikan/ditampilkan oleh JavaScript)
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 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);"; // Meminta data dari endpoint /voltage
html += " xhr.send();";
html += "}";
html += "setInterval(fetchVoltage, 2000);"; // Update setiap 2 detik
html += "fetchVoltage();"; // Panggil sekali saat halaman dimuat
// JavaScript untuk menampilkan/menyembunyikan kolom IP statis
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(); // Panggil saat halaman dimuat untuk mengatur status awal";
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 baru untuk endpoint /save_wifi
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;
}
} 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 semua pengaturan (termasuk WiFi) 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);
ESP.restart(); // Restart ESP32 agar pengaturan WiFi baru diterapkan
}
// Fungsi baru untuk endpoint /voltage
void handleGetVoltage() {
String voltageString = String(teganganAki, 2); // Ambil nilai tegangan Aki dan format ke 2 desimal
server.send(200, "text/plain", voltageString); // Kirim sebagai plain text
}
void updateDisplayForWebChanges() {
// Fungsi ini dipanggil setelah perubahan dari web untuk memastikan OLED diperbarui
displayNeedsUpdate = true;
}
No comments:
Post a Comment