/**
* PROJECT: Pump Controller V4.6.2 – Arduino UNO Edition (Production Ready)
* CHANGELOG v4.6.2 Uno:
* - Tambah Watchdog Timer (WDTO_8S) untuk proteksi hang akibat noise motor
* - EEPROM simpan totalRuntime sebagai uint32_t penuh (4 byte)
* - Semua string logging pakai F() macro untuk hemat SRAM
* - Tambah pesan startup lebih informatif untuk teknisi lapangan
* - Pinout & skema koneksi dikomentari jelas
*/
#include <EEPROM.h>
#include <avr/wdt.h>
// ==========================================
// PINOUT & SKEMA KONEKSI LAPANGAN (UNO)
// ==========================================
// Komponen | Pin UNO | Catatan
// ------------------|---------|-----------------------------------
// S1 (Toren FULL) | A0 | Pull-up internal, NO/NC via DIP SW3
// S2 (Sumur kering) | A1 | Pull-up internal, NO/NC via DIP SW4
// Fuse OK (F1) | A2 | Active LOW (hubung GND = OK)
// Reset Button | 2 | Active LOW, long press >3 detik
// DIP SW1 (Buzzer) | 3 | ON = LOW (hubung ke GND = enable)
// DIP SW2 (Relay) | 4 | ON = LOW (active LOW)
// DIP SW3 (S1 pol) | 5 | ON = LOW (NO)
// DIP SW4 (S2 pol) | 6 | ON = LOW (NO)
// Tombol Mute | 7 | Active LOW, pull-up, tekan >200 ms
// Relay Pompa | 8 | Output ke modul relay opto-isolasi
// LED Hijau | 9 | Pompa nyala
// LED Kuning | 10 | Standby / Recovery / Sumur kering
// LED Merah | 11 | Fault / Lockout
// Buzzer | 12 | Piezo aktif LOW
const uint8_t IN_S1 = A0;
const uint8_t IN_S2 = A1;
const uint8_t IN_F1 = A2;
const uint8_t IN_RESET = 2;
const uint8_t DIP_BUZZER = 3;
const uint8_t DIP_RELAY = 4;
const uint8_t DIP_S1_POL = 5;
const uint8_t DIP_S2_POL = 6;
const uint8_t BTN_MUTE = 7;
const uint8_t OUT_K1 = 8;
const uint8_t OUT_H1 = 9;
const uint8_t OUT_H2 = 10;
const uint8_t OUT_H3 = 11;
const uint8_t BUZZER = 12;
// ==========================================
// KONFIGURASI DINAMIS
bool cfgBuzzerEnabled = true;
bool cfgRelayActiveLow = true;
bool cfgS1NormallyOpen = true;
bool cfgS2NormallyOpen = true;
bool buzzerMuted = false;
unsigned long mutePressStart = 0;
bool muteFeedbackActive = false;
#define RELAY_ACTIVE (cfgRelayActiveLow ? LOW : HIGH)
#define RELAY_INACTIVE (cfgRelayActiveLow ? HIGH : LOW)
// ==========================================
// TIMERS (gunakan unsigned long agar aman overflow)
const unsigned long POWER_ON_DELAY_MS = 3000UL;
const unsigned long RECOVERY_TIME = 15UL * 60UL * 1000UL;
const unsigned long MAX_RECOVERY_TIME = 4UL * 3600UL * 1000UL;
const unsigned long MAX_RUN_TIME = 45UL * 60UL * 1000UL;
const unsigned long SUCCESS_RUN_TIME = 45UL * 1000UL;
const unsigned long MIN_RUN_ENFORCE_TIME = 20UL * 1000UL;
const unsigned long MIN_OFF_TIME = 5UL * 1000UL;
const unsigned long SENSOR_STUCK_TIME = 14400UL * 1000UL; // 4 jam
const unsigned long RESET_LONG_PRESS_MS = 3UL * 1000UL;
const unsigned long MUTE_DEBOUNCE_MS = 200UL;
const uint8_t MAX_RETRY_LIMIT = 3;
const unsigned long SCAN_INTERVAL = 100UL;
// ==========================================
// EEPROM ADDRESSES (total 5 bytes)
#define EE_DRY_RUN_COUNTER 0 // 1 byte
#define EE_RUNTIME_B0 1 // uint32_t = 4 bytes
#define EE_RUNTIME_B1 2
#define EE_RUNTIME_B2 3
#define EE_RUNTIME_B3 4
// ==========================================
// VARIABEL GLOBAL
enum SystemState { ST_INIT, ST_RUNNING, ST_RECOVERY, ST_FAULT, ST_LOCKOUT };
SystemState currentState = ST_INIT;
bool S1, S2, F1, btnReset;
bool K1_req = false;
bool minRunEnforced = false;
bool firstStartAllowed = false;
uint8_t dryRunCounter = 0;
uint32_t totalRuntime = 0;
unsigned long lastScan = 0;
unsigned long stateTimer = 0;
unsigned long motorTimer = 0;
unsigned long s2StuckTimer = 0;
unsigned long resetPressStart = 0;
unsigned long recoveryStartTime = 0;
unsigned long successfulRunTimer = 0;
unsigned long lastStopTime = 0;
// Debouncer
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;
}
} dbS1, dbS2, dbF1, dbReset, dbMute;
// ==========================================
// LOGGING RINGKAS (hemat SRAM)
#define LOGE(msg) Serial.print(F("[ERR] ")); Serial.println(F(msg))
#define LOGW(msg) Serial.print(F("[WRN] ")); Serial.println(F(msg))
#define LOGI(msg) Serial.print(F("[INF] ")); Serial.println(F(msg))
// ==========================================
// EEPROM FUNCTIONS
void loadFromEEPROM() {
dryRunCounter = EEPROM.read(EE_DRY_RUN_COUNTER);
totalRuntime = (uint32_t)EEPROM.read(EE_RUNTIME_B3) << 24 |
(uint32_t)EEPROM.read(EE_RUNTIME_B2) << 16 |
(uint32_t)EEPROM.read(EE_RUNTIME_B1) << 8 |
(uint32_t)EEPROM.read(EE_RUNTIME_B0);
}
void saveRuntimeToEEPROM() {
EEPROM.update(EE_RUNTIME_B0, totalRuntime & 0xFF);
EEPROM.update(EE_RUNTIME_B1, (totalRuntime >> 8) & 0xFF);
EEPROM.update(EE_RUNTIME_B2, (totalRuntime >> 16) & 0xFF);
EEPROM.update(EE_RUNTIME_B3, (totalRuntime >> 24) & 0xFF);
}
void saveDryRunCounter() {
EEPROM.update(EE_DRY_RUN_COUNTER, dryRunCounter);
}
// ==========================================
// INPUT READING
void readInputs() {
bool rawS1 = digitalRead(IN_S1);
bool rawS2 = digitalRead(IN_S2);
F1 = (digitalRead(IN_F1) == LOW); // asumsi fuse OK = LOW
btnReset = (digitalRead(IN_RESET) == LOW);
S1 = cfgS1NormallyOpen ? (rawS1 == LOW) : (rawS1 == HIGH);
S2 = cfgS2NormallyOpen ? (rawS2 == LOW) : (rawS2 == HIGH);
}
// ==========================================
// MUTE BUTTON + VISUAL FEEDBACK
void handleMuteButton() {
bool pressed = (digitalRead(BTN_MUTE) == LOW);
if (pressed && cfgBuzzerEnabled) {
if (mutePressStart == 0) mutePressStart = millis();
if (millis() - mutePressStart >= MUTE_DEBOUNCE_MS && !muteFeedbackActive) {
buzzerMuted = !buzzerMuted;
Serial.print(F("[INF] Buzzer mute toggle → "));
Serial.println(buzzerMuted ? F("MUTED") : F("UNMUTED"));
// Kedip kuning 2x sebagai konfirmasi
for (uint8_t i = 0; i < 2; i++) {
digitalWrite(OUT_H2, HIGH);
delay(200);
digitalWrite(OUT_H2, LOW);
delay(200);
}
muteFeedbackActive = true;
}
} else {
mutePressStart = 0;
muteFeedbackActive = false;
}
}
// ==========================================
// BUZZER CONTROL
void writeBuzzer() {
if (!cfgBuzzerEnabled || buzzerMuted) {
digitalWrite(BUZZER, BUZZER_OFF);
return;
}
unsigned long t = millis();
switch (currentState) {
case ST_FAULT: digitalWrite(BUZZER, (t % 2000UL < 1000UL) ? BUZZER_ON : BUZZER_OFF); break;
case ST_LOCKOUT: digitalWrite(BUZZER, (t % 400UL < 200UL) ? BUZZER_ON : BUZZER_OFF); break;
case ST_RECOVERY:
if (S2) digitalWrite(BUZZER, (t % 5000UL < 500UL) ? BUZZER_ON : BUZZER_OFF);
else digitalWrite(BUZZER, (t % 2000UL < 500UL) ? BUZZER_ON : BUZZER_OFF);
break;
default: digitalWrite(BUZZER, BUZZER_OFF); break;
}
}
// ==========================================
// OUTPUT CONTROL
void writeOutputs() {
digitalWrite(OUT_K1, K1_req ? RELAY_ACTIVE : RELAY_INACTIVE);
digitalWrite(OUT_H1, (currentState == ST_RUNNING && K1_req) ? HIGH : LOW);
if (currentState == ST_RECOVERY) {
if (S2) digitalWrite(OUT_H2, (millis() % 500UL < 250UL) ? HIGH : LOW);
else digitalWrite(OUT_H2, (millis() % 1000UL < 500UL) ? HIGH : LOW);
} else {
digitalWrite(OUT_H2, (currentState == ST_RUNNING && !K1_req) ? HIGH : LOW);
}
digitalWrite(OUT_H3, (currentState == ST_FAULT || currentState == ST_LOCKOUT) ? HIGH : LOW);
writeBuzzer();
}
// ==========================================
// RUNTIME COUNTER
void trackRuntime() {
static unsigned long lastTick = 0;
if (K1_req && (millis() - lastTick >= 1000UL)) {
lastTick = millis();
totalRuntime++;
if (totalRuntime % 3600UL == 0UL) {
saveRuntimeToEEPROM();
}
}
}
// ==========================================
// MAIN STATE MACHINE
void processFSM() {
// Fuse check
if (!F1) {
if (currentState != ST_FAULT) {
currentState = ST_FAULT;
K1_req = false;
LOGE("Fuse putus – Pompa dikunci");
}
return;
}
// Auto-unmute pada LOCKOUT
if (currentState == ST_LOCKOUT && buzzerMuted) {
buzzerMuted = false;
LOGI("Auto-unmute LOCKOUT – Alarm kritis aktif");
}
// Deteksi S2 macet
if (S2 && !K1_req) {
if (s2StuckTimer == 0) s2StuckTimer = millis();
if (millis() - s2StuckTimer >= SENSOR_STUCK_TIME) {
currentState = ST_FAULT;
K1_req = false;
LOGE("Sensor S2 macet >4 jam – Periksa kabel");
return;
}
} else {
s2StuckTimer = 0;
}
switch (currentState) {
case ST_INIT:
loadFromEEPROM();
readInputs();
if (S1 && S2) {
LOGW("Startup: Toren penuh + sumur kering – Monitor dulu");
}
currentState = ST_RUNNING;
LOGI("Sistem start – Ready");
break;
case ST_RUNNING:
if (!firstStartAllowed && millis() < POWER_ON_DELAY_MS) return;
firstStartAllowed = true;
if (S2 && !K1_req) {
currentState = ST_RECOVERY;
stateTimer = millis();
if (recoveryStartTime == 0) recoveryStartTime = millis();
LOGW("Sumur kering sebelum start – Masuk recovery");
return;
}
if (S2 && K1_req) {
dryRunCounter++;
if (dryRunCounter >= MAX_RETRY_LIMIT) {
currentState = ST_LOCKOUT;
LOGE("LOCKOUT – Dry-run limit tercapai");
} else {
currentState = ST_RECOVERY;
stateTimer = millis();
if (recoveryStartTime == 0) recoveryStartTime = millis();
LOGI("Recovery – Dry-run #");
Serial.println(dryRunCounter);
}
K1_req = false;
lastStopTime = millis();
successfulRunTimer = 0;
minRunEnforced = false;
saveDryRunCounter();
return;
}
// Start pompa
if (!S1 && !K1_req) {
if (millis() - lastStopTime >= MIN_OFF_TIME) {
K1_req = true;
motorTimer = millis();
successfulRunTimer = millis();
minRunEnforced = true;
LOGI("Pompa NYALA");
}
}
// Stop pompa
else if (K1_req) {
bool forceContinue = minRunEnforced && (millis() - motorTimer < MIN_RUN_ENFORCE_TIME);
bool shouldStop = S1 || (millis() - motorTimer >= MAX_RUN_TIME);
if (shouldStop && !forceContinue) {
bool overRun = (millis() - motorTimer >= MAX_RUN_TIME);
unsigned long duration = millis() - motorTimer;
K1_req = false;
lastStopTime = millis();
motorTimer = millis();
minRunEnforced = false;
if (overRun) {
currentState = ST_FAULT;
LOGE("Over-run – Cek sensor toren");
} else {
LOGI("Pompa MATI");
}
saveRuntimeToEEPROM();
}
}
// Reset dry-run counter jika sukses
if (K1_req && (millis() - successfulRunTimer >= SUCCESS_RUN_TIME) && dryRunCounter > 0) {
dryRunCounter = 0;
saveDryRunCounter();
LOGI("Dry-run counter di-reset – Stabil");
}
if (K1_req && minRunEnforced && (millis() - motorTimer >= MIN_RUN_ENFORCE_TIME)) {
minRunEnforced = false;
}
break;
case ST_RECOVERY:
if (S2) stateTimer = millis();
if (millis() - recoveryStartTime >= MAX_RECOVERY_TIME) {
currentState = ST_FAULT;
LOGE("Recovery timeout – Sumur kering permanen?");
recoveryStartTime = 0;
return;
}
if (millis() - stateTimer >= RECOVERY_TIME) {
currentState = ST_RUNNING;
recoveryStartTime = 0;
LOGI("Recovery selesai");
}
break;
case ST_FAULT:
case ST_LOCKOUT:
K1_req = false;
if (btnReset) {
if (resetPressStart == 0) resetPressStart = millis();
if (millis() - resetPressStart >= RESET_LONG_PRESS_MS) {
dryRunCounter = 0;
saveDryRunCounter();
currentState = ST_INIT;
recoveryStartTime = 0;
LOGI("Reset diterima – Kembali normal");
resetPressStart = 0;
}
} else {
resetPressStart = 0;
}
break;
}
}
// ==========================================
// SETUP
void setup() {
wdt_enable(WDTO_8S); // Watchdog 8 detik – reset jika hang
Serial.begin(115200);
delay(200);
// Baca konfigurasi DIP switch
pinMode(DIP_BUZZER, INPUT_PULLUP);
pinMode(DIP_RELAY, INPUT_PULLUP);
pinMode(DIP_S1_POL, INPUT_PULLUP);
pinMode(DIP_S2_POL, INPUT_PULLUP);
pinMode(BTN_MUTE, INPUT_PULLUP);
cfgBuzzerEnabled = (digitalRead(DIP_BUZZER) == LOW);
cfgRelayActiveLow = (digitalRead(DIP_RELAY) == LOW);
cfgS1NormallyOpen = (digitalRead(DIP_S1_POL) == LOW);
cfgS2NormallyOpen = (digitalRead(DIP_S2_POL) == LOW);
// Input pins
pinMode(IN_S1, INPUT_PULLUP);
pinMode(IN_S2, INPUT_PULLUP);
pinMode(IN_F1, INPUT_PULLUP);
pinMode(IN_RESET, INPUT_PULLUP);
// Output pins
pinMode(OUT_K1, OUTPUT); digitalWrite(OUT_K1, RELAY_INACTIVE);
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, BUZZER_OFF);
// Lamp test sederhana
digitalWrite(OUT_H1, HIGH); delay(400);
digitalWrite(OUT_H2, HIGH); delay(400);
digitalWrite(OUT_H3, HIGH); delay(400);
digitalWrite(OUT_H1, LOW); digitalWrite(OUT_H2, LOW); digitalWrite(OUT_H3, LOW);
LOGI("Pump Controller UNO v4.6.2 started");
Serial.print(F("[INF] Config: Buzzer=")); Serial.println(cfgBuzzerEnabled ? F("ON") : F("OFF"));
Serial.print(F("[INF] Relay polarity=")); Serial.println(cfgRelayActiveLow ? F("ACTIVE LOW") : F("ACTIVE HIGH"));
Serial.print(F("[INF] S1=")); Serial.print(cfgS1NormallyOpen ? F("NO") : F("NC"));
Serial.print(F(" | S2=")); Serial.println(cfgS2NormallyOpen ? F("NO") : F("NC"));
}
// ==========================================
// MAIN LOOP
void loop() {
wdt_reset(); // Kick watchdog
if (millis() - lastScan >= SCAN_INTERVAL) {
lastScan = millis();
readInputs();
handleMuteButton();
processFSM();
writeOutputs();
trackRuntime();
}
}