/*
============================================================
PROJECT : INDUSTRIAL PUMP CONTROLLER v3.2 – AUTO RECOVERY
BOARD : Arduino Nano
PURPOSE : Tank transfer for 1000L Tank & 250W Pump
VERSION : 3.2 - Optimized for Mechanical Float Hysteresis
FEATURES ADDED in 3.2:
- Auto recovery from faults after configurable cooldown (e.g., 5 min for sensor fault, 30 min for dry-run)
- Hysteresis logic for mechanical float switches (software debounce + level band to prevent chattering)
- Tuned parameters for 1000L tank: max runtime 20 min (assuming 250W pump \~50L/min flow)
- Anti-chatter for floats: require stable signal for 10s before state change
- Retained all v3.1 features (latched with auto-unlatch, EEPROM, reset cause, etc.)
DATE : March 2026
============================================================
PIN ASSIGNMENT:
- 2 : S1_TANK_NEED → LOW = tank needs water (request ON)
- 3 : S2_SUMUR_OK → LOW = sumur has water (available OK)
- 9 : PUMP_RELAY → HIGH = pump run
- 4 : LED_PUMP → Blink when running
- 5 : LED_STATUS → Heartbeat idle, off running, fast blink fault
- 7 : BUZZER → Feedback
ASSUMPTIONS:
- Sensors: Mechanical float switches with built-in hysteresis, active LOW
- Pump: 250W, \~50L/min flow → 1000L tank fill \~20 min
- For production: Add optocoupler on inputs, fuse on relay
============================================================
*/
#include <avr/wdt.h>
#include <EEPROM.h>
// ================= CONFIGURATION =================
#define DEBUG_LEVEL DEBUG_INFO // NONE / ERROR / WARN / INFO / VERBOSE
#define MAX_RUNTIME_MIN 20 // Tuned for 1000L tank & 250W pump
#define ANTI_CYCLE_OFF_SEC 30 // Min off-time
#define SENSOR_FAULT_MIN 10 // Reduced for smaller tank
#define STUCK_INPUT_HOURS 12 // Fault if stuck > X jam
#define EEPROM_FAULT_THRESHOLD 5 // Write every N fault
#define HYSTERESIS_STABLE_SEC 10 // Require stable signal for X sec (anti-chatter)
#define AUTO_RECOVER_SENSOR_MIN 5 // Auto recover sensor fault after X min
#define AUTO_RECOVER_DRY_MIN 30 // Auto recover dry-run after X min
// EEPROM addresses
#define EE_RUNTIME_HOURS 0 // uint32_t (4 bytes)
#define EE_DRY_COUNT 4 // uint16_t
#define EE_SENSOR_COUNT 6 // uint16_t
#define EE_LAST_FAULT_TYPE 8 // uint8_t (0=none,1=dry,2=sensor)
// ================= DEBUG LEVELS & LOGGING MACROS =================
enum DebugLevel { DEBUG_NONE=0, DEBUG_ERROR=1, DEBUG_WARN=2, DEBUG_INFO=3, DEBUG_VERBOSE=4 };
#if DEBUG_LEVEL >= DEBUG_ERROR
#define LOG_ERROR(...) logMessage(DEBUG_ERROR, __VA_ARGS__)
#else
#define LOG_ERROR(...)
#endif
#if DEBUG_LEVEL >= DEBUG_WARN
#define LOG_WARN(...) logMessage(DEBUG_WARN, __VA_ARGS__)
#else
#define LOG_WARN(...)
#endif
#if DEBUG_LEVEL >= DEBUG_INFO
#define LOG_INFO(...) logMessage(DEBUG_INFO, __VA_ARGS__)
#else
#define LOG_INFO(...)
#endif
#if DEBUG_LEVEL >= DEBUG_VERBOSE
#define LOG_VERBOSE(...) logMessage(DEBUG_VERBOSE, __VA_ARGS__)
#else
#define LOG_VERBOSE(...)
#endif
// ================= PIN MAPPING =================
const int PIN_S1_TANK_NEED = 2; // LOW = need water
const int PIN_S2_SUMUR_OK = 3; // LOW = water available
const int PIN_PUMP_RELAY = 9;
const int PIN_LED_PUMP = 4;
const int PIN_LED_STATUS = 5;
const int PIN_BUZZER = 7;
// ================= SYSTEM STATES =================
enum PumpState {
PUMP_IDLE, // Ready, no request
PUMP_RUNNING, // Pump active
PUMP_DRY_RUN_FAULT, // Dry-run detected, in recovery cooldown
PUMP_SENSOR_FAULT // Sensor contradiction, in recovery cooldown
};
enum BuzzerState { BUZZ_IDLE, BUZZ_ON, BUZZ_OFF };
// ================= GLOBAL MEMORY =================
PumpState currentState = PUMP_IDLE;
PumpState nextState = PUMP_IDLE;
PumpState prevState = PUMP_IDLE;
BuzzerState buzzerState = BUZZ_IDLE;
bool tankNeed_filtered = false; // true = tank needs water (S1 LOW)
bool sumurOK_filtered = false; // true = sumur OK (S2 LOW)
bool tankNeed_raw_prev = false; // For hysteresis detection
bool sumurOK_raw_prev = false;
bool blinkFast = false; // 500ms run LED
bool blinkSlow = false; // 1000ms heartbeat
bool blinkFault = false; // 200ms fault blink
unsigned long runStartTime = 0;
unsigned long offStartTime = 0;
unsigned long faultStartTime = 0;
unsigned long lastInputChange = 0;
unsigned long lastStableS1 = 0;
unsigned long lastStableS2 = 0;
unsigned long lastRuntimeUpdate = 0;
unsigned long T_BlinkFast = 0;
unsigned long T_BlinkSlow = 0;
unsigned long T_BlinkFault = 0;
unsigned long T_PeriodicBeep = 0;
unsigned long T_Buzz = 0;
unsigned long T_DebS1 = 0;
unsigned long T_DebS2 = 0;
unsigned long T_HystS1 = 0; // Hysteresis timer for S1
unsigned long T_HystS2 = 0; // Hysteresis timer for S2
// Buzzer control
int buzzerPulseRemaining = 0;
const int BUZZ_ON_MS = 100;
const int BUZZ_OFF_MS = 100;
// Diagnostics
unsigned long diag_pumpStarts = 0;
unsigned long diag_beeps = 0;
uint32_t runtimeHours = 0;
uint16_t dryFaultCount = 0;
uint16_t sensorFaultCount = 0;
uint8_t lastFaultType = 0;
// Logging
unsigned long lastLogTime = 0;
unsigned long lastStateLog = 0;
unsigned long lastBeepLog = 0;
const unsigned long LOG_RATE_MS = 100;
const unsigned long LOG_THROTTLE_STATE_MS = 500;
const unsigned long LOG_THROTTLE_BEEP_MS = 2000;
char logBuffer[64];
// ==================================================
// SETUP
// ==================================================
void setup() {
// Reset cause detection (must be first)
uint8_t resetCause = MCUSR;
MCUSR = 0; // Clear register
wdt_enable(WDTO_8S);
pinMode(PIN_S1_TANK_NEED, INPUT_PULLUP);
pinMode(PIN_S2_SUMUR_OK, INPUT_PULLUP);
pinMode(PIN_PUMP_RELAY, OUTPUT);
pinMode(PIN_LED_PUMP, OUTPUT);
pinMode(PIN_LED_STATUS, OUTPUT);
pinMode(PIN_BUZZER, OUTPUT);
digitalWrite(PIN_PUMP_RELAY, LOW);
digitalWrite(PIN_BUZZER, LOW);
digitalWrite(PIN_LED_STATUS, HIGH); // Initial heartbeat
// Load EEPROM
EEPROM.get(EE_RUNTIME_HOURS, runtimeHours);
EEPROM.get(EE_DRY_COUNT, dryFaultCount);
EEPROM.get(EE_SENSOR_COUNT, sensorFaultCount);
EEPROM.get(EE_LAST_FAULT_TYPE, lastFaultType);
#if DEBUG_LEVEL > DEBUG_NONE
Serial.begin(9600);
LOG_INFO(F("Pump Controller v3.2 started | Runtime: %lu h | Dry: %u | Sensor: %u | Last fault: %u"), runtimeHours, dryFaultCount, sensorFaultCount, lastFaultType);
// Log reset cause
if (resetCause & (1 << WDRF)) LOG_WARN(F("Reset cause: Watchdog"));
else if (resetCause & (1 << BORF)) LOG_WARN(F("Reset cause: Brownout"));
else if (resetCause & (1 << EXTRF)) LOG_WARN(F("Reset cause: External"));
else if (resetCause & (1 << PORF)) LOG_INFO(F("Reset cause: Power-on"));
else LOG_INFO(F("Reset cause: Unknown"));
#endif
// Initial input filter
tankNeed_filtered = (digitalRead(PIN_S1_TANK_NEED) == LOW);
sumurOK_filtered = (digitalRead(PIN_S2_SUMUR_OK) == LOW);
tankNeed_raw_prev = tankNeed_filtered;
sumurOK_raw_prev = sumurOK_filtered;
lastStableS1 = millis();
lastStableS2 = millis();
lastInputChange = millis();
T_HystS1 = millis();
T_HystS2 = millis();
}
// ==================================================
// MAIN LOOP (PLC SCAN CYCLE)
// ==================================================
void loop() {
wdt_reset();
inputScan(); // 1. Read & Filter Inputs + Hysteresis + Stuck Check
logicCompute(); // 2. Compute Next State
eventHandler(); // 3. Handle State Transitions + Runtime Update
outputCommit(); // 4. Update Outputs
buzzerManager(); // 5. Buzzer State Machine
logFlush(); // 6. Logging Flush
}
// ==================================================
// 1️⃣ INPUT SCAN (Debounce + Hysteresis + Clocks + Stuck Fault)
// ==================================================
void inputScan() {
unsigned long now = millis();
bool rawS1 = (digitalRead(PIN_S1_TANK_NEED) == LOW); // true = need
bool rawS2 = (digitalRead(PIN_S2_SUMUR_OK) == LOW); // true = OK
// Debounce + Hysteresis for S1 (mechanical float optimized)
if (rawS1 != tankNeed_raw_prev) {
T_HystS1 = now; // Reset hysteresis timer on change
tankNeed_raw_prev = rawS1;
} else if (now - T_HystS1 >= HYSTERESIS_STABLE_SEC * 1000UL && rawS1 != tankNeed_filtered) {
tankNeed_filtered = rawS1;
T_DebS1 = now;
lastStableS1 = now;
lastInputChange = now;
LOG_VERBOSE(F("Tank need stabilized: %d"), tankNeed_filtered);
}
// Debounce + Hysteresis for S2
if (rawS2 != sumurOK_raw_prev) {
T_HystS2 = now;
sumurOK_raw_prev = rawS2;
} else if (now - T_HystS2 >= HYSTERESIS_STABLE_SEC * 1000UL && rawS2 != sumurOK_filtered) {
sumurOK_filtered = rawS2;
T_DebS2 = now;
lastStableS2 = now;
lastInputChange = now;
LOG_VERBOSE(F("Sumur OK stabilized: %d"), sumurOK_filtered);
}
// Stuck input → escalate to SENSOR_FAULT
if (now - lastInputChange > (unsigned long)STUCK_INPUT_HOURS * 3600000UL) {
if (currentState != PUMP_SENSOR_FAULT && currentState != PUMP_DRY_RUN_FAULT) {
nextState = PUMP_SENSOR_FAULT;
LOG_WARN(F("Inputs stuck > %d hours → SENSOR_FAULT"), STUCK_INPUT_HOURS);
}
}
// Blink clocks
if (now - T_BlinkFast >= 500) {
blinkFast = !blinkFast;
T_BlinkFast = now;
}
if (now - T_BlinkSlow >= 1000) {
blinkSlow = !blinkSlow;
T_BlinkSlow = now;
}
if (now - T_BlinkFault >= 200) {
blinkFault = !blinkFault;
T_BlinkFault = now;
}
}
// ==================================================
// 2️⃣ LOGIC COMPUTE (Next State Calculation)
// ==================================================
void logicCompute() {
nextState = currentState;
unsigned long now = millis();
switch (currentState) {
case PUMP_IDLE:
if (now - offStartTime >= ANTI_CYCLE_OFF_SEC * 1000UL &&
tankNeed_filtered && sumurOK_filtered) {
nextState = PUMP_RUNNING;
}
break;
case PUMP_RUNNING:
// Dry-run override
if (!sumurOK_filtered) {
nextState = PUMP_DRY_RUN_FAULT;
}
// Normal stop
else if (!tankNeed_filtered) {
nextState = PUMP_IDLE;
}
// Max runtime (tuned for 1000L)
else if (now - runStartTime >= (unsigned long)MAX_RUNTIME_MIN * 60000UL) {
nextState = PUMP_SENSOR_FAULT;
}
// Sensor contradiction: running long but S1 unchanged
else if (now - runStartTime >= SENSOR_FAULT_MIN * 60000UL &&
tankNeed_filtered && (now - lastStableS1 >= SENSOR_FAULT_MIN * 60000UL)) { // S1 stuck true
nextState = PUMP_SENSOR_FAULT;
}
break;
case PUMP_DRY_RUN_FAULT:
// Auto recovery after cooldown
if (now - faultStartTime >= AUTO_RECOVER_DRY_MIN * 60000UL) {
if (sumurOK_filtered && tankNeed_filtered) {
nextState = PUMP_RUNNING;
} else {
nextState = PUMP_IDLE;
}
}
break;
case PUMP_SENSOR_FAULT:
// Auto recovery after shorter cooldown
if (now - faultStartTime >= AUTO_RECOVER_SENSOR_MIN * 60000UL) {
if (sumurOK_filtered && tankNeed_filtered) {
nextState = PUMP_RUNNING;
} else {
nextState = PUMP_IDLE;
}
}
break;
}
}
// ==================================================
// 3️⃣ EVENT HANDLER (Edge Detection + Diagnostics)
// ==================================================
void eventHandler() {
unsigned long now = millis();
bool stateChanged = (nextState != currentState);
if (stateChanged) {
diag_pumpStarts++; // Count transitions to running
LOG_INFO(F("State changed: %d → %d"), currentState, nextState);
}
// Entering RUNNING
if (nextState == PUMP_RUNNING && prevState != PUMP_RUNNING) {
runStartTime = now;
lastRuntimeUpdate = now;
T_PeriodicBeep = now;
LOG_INFO(F("Pump started"));
}
// Leaving RUNNING normal
if (currentState == PUMP_RUNNING && nextState == PUMP_IDLE) {
offStartTime = now;
startBuzzer(1);
LOG_INFO(F("Normal pump stop"));
}
// Dry-run fault entry
if (nextState == PUMP_DRY_RUN_FAULT && currentState != PUMP_DRY_RUN_FAULT) {
faultStartTime = now;
dryFaultCount++;
lastFaultType = 1;
if (dryFaultCount % EEPROM_FAULT_THRESHOLD == 0) {
EEPROM.put(EE_DRY_COUNT, dryFaultCount);
EEPROM.put(EE_LAST_FAULT_TYPE, lastFaultType);
LOG_VERBOSE(F("EEPROM updated for dry count"));
}
startBuzzer(3);
LOG_WARN(F("Dry-run fault | Cooldown %d min | Count: %u"), AUTO_RECOVER_DRY_MIN, dryFaultCount);
}
// Sensor fault entry
if (nextState == PUMP_SENSOR_FAULT && currentState != PUMP_SENSOR_FAULT) {
faultStartTime = now;
sensorFaultCount++;
lastFaultType = 2;
if (sensorFaultCount % EEPROM_FAULT_THRESHOLD == 0) {
EEPROM.put(EE_SENSOR_COUNT, sensorFaultCount);
EEPROM.put(EE_LAST_FAULT_TYPE, lastFaultType);
LOG_VERBOSE(F("EEPROM updated for sensor count"));
}
startBuzzer(3);
LOG_WARN(F("Sensor fault | Cooldown %d min | Count: %u"), AUTO_RECOVER_SENSOR_MIN, sensorFaultCount);
}
// Periodic 3 beeps when running
if (nextState == PUMP_RUNNING) {
if (now - T_PeriodicBeep >= 3000) {
startBuzzer(3);
T_PeriodicBeep = now;
LOG_VERBOSE(F("Periodic running beep"));
}
}
// Runtime hour meter (update every 1 min)
if (currentState == PUMP_RUNNING && now - lastRuntimeUpdate >= 60000UL) {
runtimeHours++;
EEPROM.put(EE_RUNTIME_HOURS, runtimeHours);
lastRuntimeUpdate = now;
LOG_VERBOSE(F("Pump runtime updated: %lu hours"), runtimeHours);
}
// Commit state
prevState = currentState;
currentState = nextState;
}
// ==================================================
// 4️⃣ OUTPUT COMMIT
// ==================================================
void outputCommit() {
bool running = (currentState == PUMP_RUNNING);
bool fault = (currentState == PUMP_DRY_RUN_FAULT || currentState == PUMP_SENSOR_FAULT);
digitalWrite(PIN_PUMP_RELAY, running);
digitalWrite(PIN_LED_PUMP, running && blinkFast);
// Status LED
if (fault) {
digitalWrite(PIN_LED_STATUS, blinkFault); // Fast blink on fault
} else if (running) {
digitalWrite(PIN_LED_STATUS, LOW); // Off when running
} else {
digitalWrite(PIN_LED_STATUS, blinkSlow); // Heartbeat idle
}
}
// ==================================================
// 5️⃣ BUZZER STATE MACHINE
// ==================================================
void startBuzzer(int pulses) {
diag_beeps++;
// Override ongoing if needed
if (buzzerState != BUZZ_IDLE) {
buzzerPulseRemaining = 0;
buzzerState = BUZZ_IDLE;
digitalWrite(PIN_BUZZER, LOW);
LOG_VERBOSE(F("Buzzer interrupted"));
}
buzzerPulseRemaining = pulses;
buzzerState = BUZZ_ON;
T_Buzz = millis();
LOG_VERBOSE(F("Start buzzer: %d pulses"), pulses);
}
void buzzerManager() {
unsigned long now = millis();
switch (buzzerState) {
case BUZZ_IDLE:
break;
case BUZZ_ON:
digitalWrite(PIN_BUZZER, HIGH);
if (now - T_Buzz >= BUZZ_ON_MS) {
T_Buzz = now;
buzzerState = BUZZ_OFF;
}
break;
case BUZZ_OFF:
digitalWrite(PIN_BUZZER, LOW);
if (now - T_Buzz >= BUZZ_OFF_MS) {
T_Buzz = now;
buzzerPulseRemaining--;
if (buzzerPulseRemaining > 0) {
buzzerState = BUZZ_ON;
} else {
buzzerState = BUZZ_IDLE;
}
}
break;
}
}
// ==================================================
// 6️⃣ NON-BLOCKING LOGGING
// ==================================================
void logMessage(DebugLevel level, const __FlashStringHelper* format, ...) {
if (level > DEBUG_LEVEL) return;
unsigned long now = millis();
if (now - lastLogTime < LOG_RATE_MS) return;
va_list args;
va_start(args, format);
vsnprintf_P(logBuffer, sizeof(logBuffer), (const char*)format, args);
va_end(args);
bool shouldLog = true;
if (strstr_P(logBuffer, PSTR("State"))) {
if (now - lastStateLog < LOG_THROTTLE_STATE_MS) shouldLog = false;
} else if (strstr_P(logBuffer, PSTR("buzzer"))) {
if (now - lastBeepLog < LOG_THROTTLE_BEEP_MS) shouldLog = false;
}
if (!shouldLog) return;
#if DEBUG_LEVEL > DEBUG_NONE
Serial.print(F("["));
switch (level) {
case DEBUG_ERROR: Serial.print(F("ERR")); break;
case DEBUG_WARN: Serial.print(F("WRN")); break;
case DEBUG_INFO: Serial.print(F("INF")); break;
case DEBUG_VERBOSE: Serial.print(F("VRB")); break;
default: break;
}
Serial.print(F("] "));
Serial.println(logBuffer);
#endif
lastLogTime = now;
if (strstr_P(logBuffer, PSTR("State"))) lastStateLog = now;
if (strstr_P(logBuffer, PSTR("buzzer"))) lastBeepLog = now;
}
void logFlush() {
#if DEBUG_LEVEL > DEBUG_NONE
static unsigned long lastFlush = 0;
unsigned long now = millis();
if (now - lastFlush >= 200) {
Serial.flush();
lastFlush = now;
}
#endif
}