/**
* PROJECT: OEM-Grade Pump Controller V4.4 (Single Float + ACS712 Dry-Run)
* CHANGELOG v4.4:
* - Dry-run detection menggunakan sensor arus ACS712 (bukan timeout)
* - Deteksi jika arus < threshold saat pompa nyala + float masih rendah
* - Tambah konfirmasi dry-run selama beberapa detik sebelum trip
* - Kalibrasi threshold & sensitivitas ACS712 via define
*/
#include <Arduino.h>
#include <Preferences.h>
#include <esp_task_wdt.h>
// ==========================================
// KONFIGURASI & KONSTANTA
// ==========================================
// ── POLARITAS INPUT ─────────────────────────────────────────────
#define ACTIVE_HIGH_LOGIC // sesuaikan dengan float switch Anda
#ifdef ACTIVE_HIGH_LOGIC
#define INPUT_ACTIVE(x) (!(x)) // pull-up → LOW = aktif
#else
#define INPUT_ACTIVE(x) (x)
#endif
// Pin konfigurasi
const uint8_t IN_S1 = 33; // SINGLE FLOAT SWITCH (rendah = minta nyala)
const uint8_t IN_F1 = 27; // Fuse OK
const uint8_t IN_RESET = 26; // Reset button
const uint8_t ACS712_PIN = 34; // Analog pin untuk ACS712 (OUT)
const uint8_t OUT_K1 = 25;
const uint8_t OUT_H1 = 18; // Hijau - Pompa nyala
const uint8_t OUT_H2 = 19; // Kuning - Standby
const uint8_t OUT_H3 = 23; // Merah - Fault/Lockout
const uint8_t BUZZER = 14;
#define RELAY_ON LOW
#define RELAY_OFF HIGH
// Timer (ms)
const uint32_t MIN_RUN_TIME = 15 * 1000; // minimal nyala
const uint32_t MIN_OFF_TIME = 8 * 1000; // anti chattering
const uint32_t DRY_RUN_CONFIRM_MS = 10 * 1000; // konfirmasi dry-run selama 10 detik
const uint32_t RECOVERY_TIME = 15 * 60 * 1000;
const uint32_t RESET_LONG_PRESS_MS = 3 * 1000;
// ACS712 – sesuaikan dengan tipe modul Anda
#define ACS712_SENSITIVITY_MV_PER_A 185.0 // 185 untuk 5A, 100 untuk 20A, 66 untuk 30A
#define VREF 3.3 // jika pakai 3.3V ESP32, ubah ke 3.3; jika 5V → 5.0
#define ADC_RESOLUTION 4095.0 // ESP32 12-bit
// AMBANG DRY-RUN – HARUS DIKALIBRASI SENDIRI!
// Contoh: pompa normal 3.2A → dry < 1.0A
const float DRY_RUN_CURRENT_THRESHOLD = 1.0; // Ampere (ubah sesuai pompa Anda!)
const uint8_t MAX_RETRY_LIMIT = 4;
const uint32_t SCAN_INTERVAL = 100;
const uint32_t WDT_TIMEOUT_MS = 5000;
// ==========================================
// DEBUG LOGGING
// ==========================================
enum DebugLevel { LOG_ERROR, LOG_WARN, LOG_INFO, LOG_DEBUG };
DebugLevel currentLevel = LOG_INFO;
const unsigned long LOG_INTERVAL = 250;
unsigned long lastLogTime = 0;
void logMessage(DebugLevel level, const char* format, ...) {
if (level > currentLevel) return;
unsigned long now = millis();
if (now - lastLogTime < LOG_INTERVAL) return;
lastLogTime = now;
char buffer[160];
va_list args;
va_start(args, format);
vsnprintf(buffer, sizeof(buffer), format, args);
va_end(args);
switch (level) {
case LOG_ERROR: Serial.print("[ERROR] "); break;
case LOG_WARN: Serial.print("[WARN ] "); break;
case LOG_INFO: Serial.print("[INFO ] "); break;
case LOG_DEBUG: Serial.print("[DEBUG] "); break;
}
Serial.println(buffer);
}
#define LOGE(...) logMessage(LOG_ERROR, __VA_ARGS__)
#define LOGW(...) logMessage(LOG_WARN, __VA_ARGS__)
#define LOGI(...) logMessage(LOG_INFO, __VA_ARGS__)
#define LOGD(...) logMessage(LOG_DEBUG, __VA_ARGS__)
// ==========================================
// STATUS & VARIABEL
// ==========================================
enum SystemState { ST_INIT, ST_RUNNING, ST_RECOVERY, ST_FAULT, ST_LOCKOUT };
SystemState currentState = ST_INIT;
bool floatLow, F1, btnReset;
bool pumpRequest = false;
uint8_t dryRunCounter = 0;
uint64_t totalRuntime = 0;
unsigned long lastScan = 0;
unsigned long motorTimer = 0;
unsigned long dryRunDetectStart = 0;
unsigned long stateTimer = 0;
unsigned long resetPressStart = 0;
struct Debouncer {
uint8_t count = 0;
bool state = false;
bool update(bool raw) {
if (raw) { if (count < 5) count++; } else { if (count > 0) count--; }
if (count == 5) state = true;
else if (count == 0) state = false;
return state;
}
} dbFloat, dbF1, dbReset;
Preferences prefs;
// ==========================================
// FUNGSI BACA ARUS ACS712
// ==========================================
float readCurrent() {
const int SAMPLES = 300; // rata-rata untuk stabil (khususnya AC)
float sum = 0.0;
for (int i = 0; i < SAMPLES; i++) {
sum += analogRead(ACS712_PIN);
delayMicroseconds(100); // ~3 ms total sampling
}
float adcValue = sum / SAMPLES;
float voltage = (adcValue / ADC_RESOLUTION) * VREF;
// Hitung arus (bisa positif/negatif, kita ambil absolut)
float current = (voltage - (VREF / 2.0)) / (ACS712_SENSITIVITY_MV_PER_A / 1000.0);
return abs(current);
}
// ==========================================
// FSM & LOGIKA UTAMA
// ==========================================
void readInputs() {
floatLow = INPUT_ACTIVE(dbFloat.update(digitalRead(IN_S1))); // true = tangki rendah
F1 = INPUT_ACTIVE(dbF1.update(digitalRead(IN_F1)));
btnReset = INPUT_ACTIVE(dbReset.update(digitalRead(IN_RESET)));
}
void processFSM() {
if (!F1) {
if (currentState != ST_FAULT) {
currentState = ST_FAULT;
pumpRequest = false;
LOGE("FAULT: FUSE PUTUS | Pompa dikunci");
}
return;
}
switch (currentState) {
case ST_INIT:
dryRunCounter = prefs.getUChar("retries", 0);
totalRuntime = prefs.getULong64("runtime", 0ULL);
currentState = ST_RUNNING;
LOGI("START | Retries: %d | Runtime: %llu jam", dryRunCounter, totalRuntime / 3600);
break;
case ST_RUNNING:
// Logika pompa hysteresis
if (floatLow) {
if (!pumpRequest && (millis() - motorTimer >= MIN_OFF_TIME)) {
pumpRequest = true;
motorTimer = millis();
dryRunCounter = 0;
prefs.putUChar("retries", 0);
dryRunDetectStart = 0; // reset deteksi
LOGI("POMPA MENYALA | Float rendah");
}
} else {
if (pumpRequest && (millis() - motorTimer >= MIN_RUN_TIME)) {
pumpRequest = false;
motorTimer = millis();
LOGI("POMPA MATI | Float cukup");
}
}
// Deteksi dry-run via arus (hanya saat pompa nyala + float masih rendah)
if (pumpRequest && floatLow) {
float current = readCurrent();
LOGD("Arus saat ini: %.2f A", current);
if (current < DRY_RUN_CURRENT_THRESHOLD) {
if (dryRunDetectStart == 0) {
dryRunDetectStart = millis();
LOGW("Arus rendah terdeteksi (%.2f A) → mulai konfirmasi dry-run", current);
}
if (millis() - dryRunDetectStart >= DRY_RUN_CONFIRM_MS) {
dryRunCounter++;
prefs.putUChar("retries", dryRunCounter);
LOGW("DRY-RUN TERKONFIRMASI #%d/%d | Arus %.2f A < %.2f A",
dryRunCounter, MAX_RETRY_LIMIT, current, DRY_RUN_CURRENT_THRESHOLD);
pumpRequest = false;
motorTimer = millis();
dryRunDetectStart = 0;
if (dryRunCounter >= MAX_RETRY_LIMIT) {
currentState = ST_LOCKOUT;
LOGE("LOCKOUT AKTIF | Terlalu banyak dry-run (%d)", MAX_RETRY_LIMIT);
} else {
currentState = ST_RECOVERY;
stateTimer = millis();
LOGI("MASUK RECOVERY %lu menit", RECOVERY_TIME / 60000UL);
}
}
} else {
dryRunDetectStart = 0; // reset jika arus kembali normal
}
} else {
dryRunDetectStart = 0;
}
break;
case ST_RECOVERY:
pumpRequest = false;
if (millis() - stateTimer >= RECOVERY_TIME) {
currentState = ST_RUNNING;
LOGI("Recovery selesai");
}
break;
case ST_FAULT:
case ST_LOCKOUT:
pumpRequest = false;
if (btnReset) {
if (resetPressStart == 0) resetPressStart = millis();
if (millis() - resetPressStart >= RESET_LONG_PRESS_MS) {
dryRunCounter = 0;
prefs.putUChar("retries", 0);
currentState = ST_INIT;
LOGI("RESET DITERIMA | Sistem normal kembali");
resetPressStart = 0;
}
} else {
resetPressStart = 0;
}
break;
}
}
void writeOutputs() {
digitalWrite(OUT_K1, pumpRequest ? RELAY_ON : RELAY_OFF);
digitalWrite(OUT_H1, pumpRequest);
digitalWrite(OUT_H2, (currentState == ST_RUNNING && !pumpRequest));
digitalWrite(OUT_H3, (currentState == ST_FAULT) ||
(currentState == ST_LOCKOUT && (millis() % 400 < 200)));
bool alarm = (currentState == ST_FAULT || currentState == ST_LOCKOUT);
digitalWrite(BUZZER, alarm && (millis() % 4000 < 800));
}
void trackRuntime() {
static unsigned long lastTick = 0;
if (pumpRequest && (millis() - lastTick >= 1000)) {
lastTick = millis();
totalRuntime++;
if (totalRuntime % 3600 == 0) {
prefs.putULong64("runtime", totalRuntime);
LOGD("Runtime disimpan: %llu jam", totalRuntime / 3600);
}
}
}
// ==========================================
// SETUP & LOOP
// ==========================================
void setup() {
Serial.begin(115200);
delay(150);
esp_task_wdt_config_t wdt_config = {
.timeout_ms = WDT_TIMEOUT_MS,
.idle_core_mask = 0,
.trigger_panic = true
};
esp_task_wdt_init(&wdt_config);
esp_task_wdt_add(NULL);
prefs.begin("pump-ctrl", false);
pinMode(IN_S1, INPUT_PULLUP);
pinMode(IN_F1, INPUT_PULLUP);
pinMode(IN_RESET, INPUT_PULLUP);
pinMode(ACS712_PIN, INPUT);
digitalWrite(OUT_K1, RELAY_OFF);
pinMode(OUT_K1, OUTPUT);
pinMode(OUT_H1, OUTPUT); digitalWrite(OUT_H1, LOW);
pinMode(OUT_H2, OUTPUT); digitalWrite(OUT_H2, LOW);
pinMode(OUT_H3, OUTPUT); digitalWrite(OUT_H3, LOW);
pinMode(BUZZER, OUTPUT); digitalWrite(BUZZER, LOW);
// Lamp test
digitalWrite(OUT_H1, HIGH); digitalWrite(OUT_H2, HIGH); digitalWrite(OUT_H3, HIGH);
delay(600);
digitalWrite(OUT_H1, LOW); digitalWrite(OUT_H2, LOW); digitalWrite(OUT_H3, LOW);
LOGI("Pump Controller V4.4 (Single Float + ACS712) STARTED");
LOGI("Dry-run threshold: %.2f A | Sensitivitas ACS712: %.0f mV/A", DRY_RUN_CURRENT_THRESHOLD, ACS712_SENSITIVITY_MV_PER_A);
}
void loop() {
esp_task_wdt_reset();
if (millis() - lastScan >= SCAN_INTERVAL) {
lastScan = millis();
readInputs();
processFSM();
writeOutputs();
trackRuntime();
}
}