Search This Blog

Saturday, 26 July 2025

CARGER AKI CONTROLER INA226-LIBARY WE ROTARI SW+OLED 0.96 SETING WEB OKE PLUS ATUR DESIMAL VOLTASE + ATUR IP STATIK

 #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

Tandon Air Otomatis (ESP32 Master & Display) + Kode Lengkap

  Panduan Lengkap Tandon Air Otomatis (ESP32 Master & Display) + Kode Lengkap Diperbarui: 09 August 2025 Artikel ini memandu Anda memban...