#include <avr/wdt.h>
#include <EEPROM.h>
#include <math.h>
#include <stdarg.h>
#include <TM1637Display.h>
// ================= PRODUCTION BUILD – LOGGING OFF =================
#define PRODUCTION_BUILD
#ifdef PRODUCTION_BUILD
#define LOG_ERROR(...) do{}while(0)
#define LOG_WARN(...) do{}while(0)
#define LOG_INFO(...) do{}while(0)
#define LOG_DEBUG(...) do{}while(0)
#define LOG_TRACE(...) do{}while(0)
#endif
#define FIRMWARE_VERSION "v5.3.4 – Final Production Ready"
// ================= SENSOR TYPE =================
#define SENSOR_TYPE 20
#if SENSOR_TYPE == 5
#define ACS_SENS 0.185f
#define INITIAL_NO_LOAD_A 0.65f
#elif SENSOR_TYPE == 20
#define ACS_SENS 0.100f
#define INITIAL_NO_LOAD_A 1.20f
#elif SENSOR_TYPE == 30
#define ACS_SENS 0.066f
#define INITIAL_NO_LOAD_A 2.40f
#else
#error "SENSOR_TYPE harus 5, 20 atau 30"
#endif
// ================= KONSTANTA =================
const float OC_WARNING_FACTOR = 2.20f;
const float OC_TRIP_FACTOR = 2.80f;
#define VOLTAGE_DIV_RATIO (13.3f / 3.3f)
const uint32_t AUTO_LEARN_FIRST_MS = 300000UL;
const float AGING_ALLOWANCE = 1.02f;
const float AGING_SLOW_RATE = 0.005f;
const float MAX_BASELINE_INCREASE = 1.15f;
const uint16_t RESET_FORCE_RELEARN_MS = 8000UL;
const uint32_t SERVICE_PAGE_TIME = 3500UL;
const uint32_t RAPID_RESTART_WINDOW_MS = 120000UL;
const uint8_t RAPID_RESTART_MAX = 5;
const uint32_t LEARN_DISPLAY_TIME_MS = 2800UL;
const uint8_t IO_S1_TANK = 2;
const uint8_t IO_S2_DRY = 4;
const uint8_t IO_RESET_BTN = A0;
const uint8_t IO_CT_CURRENT = A6;
const uint8_t IO_VOLTAGE = A7;
const uint8_t IO_R1_DRY = 9;
const uint8_t IO_R2_PUMP = 10;
const uint8_t IO_LED_PUMP = 5;
const uint8_t IO_LED_SAFE = 6;
const uint8_t IO_LED_STATUS = 7;
const uint8_t IO_BUZZER = 3;
const uint8_t TM_CLK = 11;
const uint8_t TM_DIO = 12;
TM1637Display display(TM_CLK, TM_DIO);
const uint8_t LED_PINS[3] = {IO_LED_PUMP, IO_LED_SAFE, IO_LED_STATUS};
const uint8_t DIP_CURRENT_EN = A1;
const uint8_t DIP_DRY_EN = A2;
const uint8_t DIP_VOLTAGE_EN = A3;
const uint32_t SCAN_CYCLE = 50;
const uint32_t WELL_RECOVERY = 10000UL;
const uint32_t REQUEST_TIMEOUT = 10000UL;
const uint32_t START_DELAY_MS = 800UL;
const uint32_t NOLOAD_HYST_TIME = 3500UL;
const uint32_t OC_HYST_TIME = 5000UL;
const uint32_t STALL_HYST_TIME = 3000UL;
const uint32_t WELD_CHECK_TIME = 2000UL;
const uint32_t MIN_OFF_TIME_MS = 15000UL;
const uint32_t OC_STARTUP_IGNORE = 2500UL;
const uint32_t NOLOAD_STARTUP_IGNORE = 4500UL;
const uint32_t MIN_RUN_TIME_MS = 60000UL;
const uint32_t STABILIZATION_TIME = 15000UL;
const uint32_t DRY_WARNING_DROP_TIME = 8000UL;
const float ADAPTIVE_FACTOR = 0.55f;
const float DRY_DROP_THRESHOLD_BASE = 0.38f;
const float DRY_WARNING_DROP_BASE = 0.25f;
const float STALL_FACTOR = 1.65f;
const float HEALTH_DEGRADE_FACTOR = 1.20f;
const float RELAY_WELD_THRESHOLD = 0.40f;
const float BROWNOUT_THRESHOLD = 10.2f;
const float BROWNOUT_HYSTERESIS = 0.4f;
const uint8_t MAX_RETRY = 5;
const uint32_t HOUR_TICK_MS = 3600000UL;
const uint32_t BUZZER_SLOW_PERIOD = 10000UL;
const uint32_t BUZZER_SLOW_ON = 2000UL;
const uint32_t BUZZER_FAST_PERIOD = 1000UL;
const uint32_t BUZZER_FAST_ON = 200UL;
const uint32_t BUZZER_NOLOAD_PERIOD = 2000UL;
const uint32_t BUZZER_SOS_PERIOD = 6000UL;
const uint32_t BUZZER_MAX_TIME_MS = 1800000UL;
const uint32_t DISPLAY_SWITCH_MS = 2500UL;
const uint16_t RESET_HOLD_MS = 1000;
const uint16_t RESET_LONG_MS = 5000;
const uint16_t RESET_DEBOUNCE_MS = 50;
const uint32_t AUTO_CALIB_IDLE_MS = 600000UL;
const uint32_t SOURCE_DRY_HYST = 60000UL;
const uint32_t EEPROM_FAULT_THROTTLE = 10000UL;
const uint32_t SERVICE_MODE_HOLD_MS = 3000UL;
const uint32_t MIN_EEPROM_SAVE_MS = 30000UL;
const uint8_t TM_BRIGHTNESS = 0x0A;
const uint32_t BROWNOUT_IGNORE_START_MS = 1500UL;
#define AUTO_LEARN_HOLD_MS 3000UL
#define AUTO_LEARN_STABILIZE_MS 12000UL
#define AUTO_LEARN_SAMPLES 40
const int EE_RUNTIME_SLOTS[4] = {0, 6, 12, 18};
const int EE_FAULT_INDEX = 24;
const int EE_FAULT_LOG = 26;
const int EE_EEPROM_INDEX = 186;
const int EE_GLOBAL_CRC = 187;
const int EE_ACS_OFFSET = 194;
const int EE_OFFSET_VALID = 198;
const int EE_START_COUNT = 200;
const int EE_BASELINE_CURRENT = 204;
const int EE_DRY_SENSITIVITY = 210;
const int EE_LEARNED_FLAG = 220;
const int EE_LEARNED_BASELINE = 222;
const uint16_t BLINK_DRY_RUN[] = {150, 150, 0};
const uint16_t BLINK_LATCHED[] = {800, 400, 0};
const uint16_t BLINK_OTHER_FAULT[] = {400, 400, 0};
const uint16_t BLINK_CALIBRATE[] = {100, 100, 0};
const uint16_t BLINK_SOURCE_DRY[] = {300, 300, 0};
const uint16_t BLINK_HEALTH_WARN[] = {500, 500, 0};
const uint16_t BLINK_OFF[] = {0, 0, 0};
enum PumpState {
ST_INIT, ST_CALIBRATE, ST_IDLE, ST_REQUEST,
ST_STARTING, ST_RUNNING, ST_DRY_FAULT, ST_NOLOAD_FAULT,
ST_OVERCURRENT_FAULT, ST_STALL_FAULT, ST_RELAY_WELD, ST_BROWNOUT, ST_LATCHED
};
PumpState state = ST_INIT;
PumpState prevState = ST_INIT;
struct SensorData {
bool tankRaw, tankDeb, tankNeedsWater; unsigned long tankDebTime;
bool wellRaw, wellDeb, wellHasWater; unsigned long wellDebTime;
unsigned long wellRecoveryTimer, requestEntryTime, startDelayTimer;
float offset, rms, rmsFiltered, rmsSlowFiltered, runningCurrentAvg;
float baselineCurrent, lastSavedBaseline, supplyVoltage, supplyVoltageFiltered;
bool currentStabilized, noLoad, overCurrentRaw, overCurrentWarning, stallRaw;
bool currentDropDry, currentDropWarn, healthWarning, relayWeldDetected;
unsigned long tNoLoadHyst, tOverCurrentHyst, tStallHyst, tDropWarn, relayWeldTimer;
unsigned long pumpStartTime, minRunStart, lastStopTime;
bool pumpJustStarted, calibrated;
unsigned long lastIdleCalib, sourceDryWarningTimer;
bool sourceDryWarningActive;
float lastFaultCurrent;
uint8_t drySensitivity;
bool autoLearnActive; unsigned long autoLearnStartTime; float learnSumCurrent;
uint16_t learnSampleCount; bool learned;
bool autoLearnTriggered; unsigned long firstRunStartTime;
bool learnDisplayActive; unsigned long learnDisplayTimer; uint8_t learnMessageType;
} sensor = {0};
struct ProtectionData { uint8_t retryDry, retryNoLoad, retryOC, retryStall, retryWeld; } protect = {0};
struct RuntimeData {
uint32_t hours, accumulatedMs; uint32_t startCount; unsigned long lastTick;
uint8_t faultIndex, eepromIndex;
} runtime = {0};
unsigned long recentStartTimes[5] = {0}; uint8_t recentStartIndex = 0;
uint8_t lastFault = 0; unsigned long lastFaultLogTime = 0;
bool inServiceMode = false; uint8_t servicePage = 0; unsigned long servicePageTimer = 0;
bool enableCurrentSensor = true, enableDrySensor = true, enableVoltageDetection = true;
unsigned long scanTimer = 0, lastEEPROMSave = 0;
bool buzzerMutedByUser = false; unsigned long muteStartTime = 0;
bool resetPressed = false; unsigned long resetPressStart = 0;
unsigned long buzzerStartTime = 0, buzzerLatchedStart = 0;
const uint32_t MUTE_TIMEOUT_MS = 900000UL;
// ================= CRC & EEPROM =================
uint16_t crc16(const uint8_t* data, uint16_t len) {
uint16_t crc = 0xFFFF;
for (uint16_t i = 0; i < len; i++) {
crc ^= data[i];
for (uint8_t j = 0; j < 8; j++)
crc = (crc & 1) ? (crc >> 1) ^ 0xA001 : (crc >> 1);
}
return crc;
}
void saveEEPROM(bool force = false) {
unsigned long now = millis();
if (!force && now - lastEEPROMSave < MIN_EEPROM_SAVE_MS) return;
runtime.eepromIndex = (runtime.eepromIndex + 1) % 4;
int slot = EE_RUNTIME_SLOTS[runtime.eepromIndex];
uint8_t buf[4];
for (uint8_t i = 0; i < 4; i++) buf[i] = (runtime.hours >> (i*8)) & 0xFF;
uint16_t slotCRC = crc16(buf, 4);
uint8_t gbuf[14];
memcpy(gbuf, buf, 4);
gbuf[4] = runtime.faultIndex;
gbuf[5] = runtime.eepromIndex;
memcpy(&gbuf[6], &runtime.startCount, 4);
memcpy(&gbuf[10], &sensor.baselineCurrent, 4);
uint16_t globalCRC = crc16(gbuf, 14);
noInterrupts();
for (uint8_t i = 0; i < 4; i++) EEPROM.update(slot + i, buf[i]);
EEPROM.put(slot + 4, slotCRC);
EEPROM.update(EE_FAULT_INDEX, runtime.faultIndex);
EEPROM.update(EE_EEPROM_INDEX, runtime.eepromIndex);
EEPROM.put(EE_START_COUNT, runtime.startCount);
EEPROM.put(EE_GLOBAL_CRC, globalCRC);
if (force || fabs(sensor.baselineCurrent - sensor.lastSavedBaseline) > 0.05f) {
EEPROM.put(EE_BASELINE_CURRENT, sensor.baselineCurrent);
sensor.lastSavedBaseline = sensor.baselineCurrent;
}
interrupts();
lastEEPROMSave = now;
}
void loadEEPROM() {
EEPROM.get(EE_EEPROM_INDEX, runtime.eepromIndex);
if (runtime.eepromIndex > 3) runtime.eepromIndex = 0;
uint32_t best = 0; uint8_t bestSlot = 0;
for (uint8_t s = 0; s < 4; s++) {
int addr = EE_RUNTIME_SLOTS[s];
uint32_t h = 0; uint16_t c;
for (uint8_t i = 0; i < 4; i++) h |= ((uint32_t)EEPROM.read(addr + i) << (i*8));
EEPROM.get(addr + 4, c);
uint8_t tmp[4]; for (uint8_t i = 0; i < 4; i++) tmp[i] = (h >> (i*8)) & 0xFF;
if (crc16(tmp, 4) == c && h > best) { best = h; bestSlot = s; }
}
runtime.hours = best; runtime.eepromIndex = bestSlot; runtime.accumulatedMs = 0;
EEPROM.get(EE_FAULT_INDEX, runtime.faultIndex);
EEPROM.get(EE_START_COUNT, runtime.startCount);
EEPROM.get(EE_BASELINE_CURRENT, sensor.baselineCurrent);
uint8_t learnedFlag = 0; EEPROM.get(EE_LEARNED_FLAG, learnedFlag);
sensor.learned = (learnedFlag == 1);
if (sensor.learned) {
EEPROM.get(EE_LEARNED_BASELINE, sensor.baselineCurrent);
sensor.runningCurrentAvg = sensor.baselineCurrent;
sensor.lastSavedBaseline = sensor.baselineCurrent;
}
uint8_t valid = 0; EEPROM.get(EE_OFFSET_VALID, valid);
if (valid == 1) {
float temp; EEPROM.get(EE_ACS_OFFSET, temp);
if (temp >= 2.0f && temp <= 3.0f) { sensor.offset = temp; sensor.calibrated = true; }
}
uint8_t sens = 1; EEPROM.get(EE_DRY_SENSITIVITY, sens);
sensor.drySensitivity = (sens <= 2) ? sens : 1;
}
// ================= AUTO-LEARN & FAULT =================
void performAutoLearn(bool isAuto = false) {
if (state != ST_RUNNING || sensor.autoLearnActive) return;
sensor.autoLearnActive = true;
sensor.autoLearnStartTime = millis();
sensor.learnSumCurrent = 0.0f;
sensor.learnSampleCount = 0;
if (!isAuto) {
uint8_t seg[4] = {0x38, 0x50, 0x71, 0x54};
display.setSegments(seg);
}
}
void logFault(uint8_t type) {
unsigned long now = millis();
if (now - lastFaultLogTime < EEPROM_FAULT_THROTTLE) return;
lastFaultLogTime = now;
lastFault = type;
uint32_t totalMin = runtime.hours * 60 + (runtime.accumulatedMs / 60000UL);
uint8_t hrsLow = runtime.hours & 0xFF;
uint8_t min = totalMin % 60;
sensor.lastFaultCurrent = sensor.rmsSlowFiltered;
int16_t current10 = (int16_t)(sensor.lastFaultCurrent * 10.0f + 0.5f);
current10 = constrain(current10, -999, 999);
uint8_t data[5] = {type, hrsLow, min, (uint8_t)(current10 & 0xFF), (uint8_t)(current10 >> 8)};
uint16_t ecrc = crc16(data, 5);
int addr = EE_FAULT_LOG + runtime.faultIndex * 6;
noInterrupts();
for (uint8_t i = 0; i < 5; i++) EEPROM.update(addr + i, data[i]);
EEPROM.put(addr + 5, ecrc);
interrupts();
runtime.faultIndex = (runtime.faultIndex + 1) % 32;
saveEEPROM(true);
}
// ================= SERVICE MODE =================
void enterServiceMode() {
inServiceMode = true;
servicePage = 0;
servicePageTimer = millis();
display.clear();
}
void serviceModeLoop() {
if (!inServiceMode) return;
unsigned long now = millis();
if (now - servicePageTimer < SERVICE_PAGE_TIME) return;
servicePageTimer = now;
display.clear();
switch (servicePage) {
case 0: display.showNumberDecEx((uint16_t)(sensor.offset * 100), 0x40, false, 4, 0); break;
case 1: display.showNumberDecEx((uint16_t)(sensor.baselineCurrent * 10), 0, false, 4, 0); break;
case 2: display.showNumberDec((uint16_t)runtime.startCount, false, 4, 0); break;
case 3: display.showNumberDecEx((uint16_t)(sensor.supplyVoltageFiltered * 10), 0x40, false, 4, 0); break;
case 4: display.showNumberDec(sensor.drySensitivity, false, 1, 3); break;
case 5: display.showNumberDec(runtime.faultIndex, false, 2, 0); break;
case 6: display.showNumberDec(min(runtime.hours, 9999UL), false, 4, 0); break;
case 7: {
uint8_t seg[4] = {0x39, 0x5C, 0x3E, 0x00};
if (enableCurrentSensor) seg[0] = 0x39; else seg[0] = 0x00;
if (enableDrySensor) seg[1] = 0x5C; else seg[1] = 0x00;
if (enableVoltageDetection) seg[2] = 0x3E; else seg[2] = 0x00;
display.setSegments(seg);
break;
}
}
servicePage++;
if (servicePage >= 8) {
inServiceMode = false;
display.clear();
}
}
// ================= LED PATTERN =================
void setLedPattern(uint8_t pin, const uint16_t pattern[]) {
static const uint16_t* curPat[3] = {BLINK_OFF, BLINK_OFF, BLINK_OFF};
static uint8_t idx[3] = {0,0,0};
static unsigned long lastCh[3] = {0,0,0};
uint8_t i = 255;
for (uint8_t k = 0; k < 3; k++) if (LED_PINS[k] == pin) { i = k; break; }
if (i == 255) return;
unsigned long now = millis();
if (pattern != curPat[i]) {
curPat[i] = pattern; idx[i] = 0; lastCh[i] = now; digitalWrite(pin, HIGH);
}
if (curPat[i][0] == 0) { digitalWrite(pin, LOW); return; }
uint8_t len = (curPat[i][2] != 0) ? 3 : 2;
if (now - lastCh[i] >= curPat[i][idx[i]]) {
lastCh[i] = now;
idx[i] = (idx[i] + 1) % len;
digitalWrite(pin, (idx[i] == 0) ? HIGH : LOW);
}
}
// ================= SETUP =================
void setup() {
uint8_t mcusr = MCUSR; MCUSR = 0;
wdt_disable(); wdt_enable(WDTO_4S);
Serial.begin(115200);
analogReference(DEFAULT);
display.setBrightness(TM_BRIGHTNESS); display.clear();
pinMode(IO_S1_TANK, INPUT_PULLUP); pinMode(IO_S2_DRY, INPUT_PULLUP);
pinMode(IO_RESET_BTN, INPUT_PULLUP); pinMode(IO_CT_CURRENT, INPUT);
pinMode(IO_VOLTAGE, INPUT);
pinMode(IO_R1_DRY, OUTPUT); pinMode(IO_R2_PUMP, OUTPUT);
pinMode(IO_LED_PUMP, OUTPUT); pinMode(IO_LED_SAFE, OUTPUT);
pinMode(IO_LED_STATUS, OUTPUT); pinMode(IO_BUZZER, OUTPUT);
pinMode(DIP_CURRENT_EN, INPUT_PULLUP); pinMode(DIP_DRY_EN, INPUT_PULLUP);
pinMode(DIP_VOLTAGE_EN, INPUT_PULLUP);
enableCurrentSensor = (digitalRead(DIP_CURRENT_EN) == LOW);
enableDrySensor = (digitalRead(DIP_DRY_EN) == LOW);
enableVoltageDetection = (digitalRead(DIP_VOLTAGE_EN) == LOW);
if (digitalRead(IO_RESET_BTN) == LOW) {
unsigned long hold = millis();
while (digitalRead(IO_RESET_BTN) == LOW && millis() - hold < SERVICE_MODE_HOLD_MS);
if (millis() - hold >= SERVICE_MODE_HOLD_MS) inServiceMode = true;
}
digitalWrite(IO_LED_SAFE, HIGH); delay(200);
digitalWrite(IO_LED_PUMP, HIGH); delay(200);
digitalWrite(IO_LED_STATUS, HIGH); delay(200);
digitalWrite(IO_BUZZER, HIGH); delay(300);
digitalWrite(IO_LED_SAFE, LOW); digitalWrite(IO_LED_PUMP, LOW);
digitalWrite(IO_LED_STATUS, LOW); digitalWrite(IO_BUZZER, LOW);
digitalWrite(IO_R1_DRY, HIGH); delay(80); digitalWrite(IO_R1_DRY, LOW);
digitalWrite(IO_R2_PUMP, HIGH); delay(80); digitalWrite(IO_R2_PUMP, LOW);
loadEEPROM();
if (!(mcusr & (1<<WDRF))) {
protect.retryDry = protect.retryNoLoad = protect.retryOC = protect.retryStall = protect.retryWeld = 0;
}
memset(recentStartTimes, 0, sizeof(recentStartTimes));
recentStartIndex = 0;
sensor.autoLearnTriggered = false;
sensor.firstRunStartTime = 0;
sensor.learnDisplayActive = false;
runtime.lastTick = millis();
if (!enableCurrentSensor) {
sensor.calibrated = true; sensor.offset = 2.5f;
} else if (!sensor.calibrated) sensor.offset = 2.5f;
sensor.rmsFiltered = 3.0f; sensor.rmsSlowFiltered = 3.0f;
sensor.runningCurrentAvg = INITIAL_NO_LOAD_A * 2.0f;
sensor.baselineCurrent = sensor.baselineCurrent > 0 ? sensor.baselineCurrent : 0.0f;
sensor.lastSavedBaseline = sensor.baselineCurrent;
if (!enableVoltageDetection) sensor.supplyVoltageFiltered = 12.5f;
else sensor.supplyVoltageFiltered = 12.0f;
sensor.lastIdleCalib = millis();
sensor.lastStopTime = millis() - MIN_OFF_TIME_MS;
if (inServiceMode) enterServiceMode();
}
// ================= MAIN LOOP =================
void loop() {
wdt_reset();
unsigned long now = millis();
if (now - scanTimer >= SCAN_CYCLE) {
scanTimer = now;
plcCycle();
updateHMI();
}
if (inServiceMode) serviceModeLoop();
updateBuzzer();
}
// ================= PLC CYCLE =================
void plcCycle() {
unsigned long now = millis();
if (prevState == ST_RUNNING || prevState == ST_STARTING) {
runtime.accumulatedMs += (now - runtime.lastTick);
runtime.lastTick = now;
}
readInputs();
fsm();
outputs();
checkRelayWeld();
hourMeter();
if (state == ST_STARTING && prevState != ST_STARTING) {
recentStartTimes[recentStartIndex] = now;
recentStartIndex = (recentStartIndex + 1) % 5;
bool rapid = true;
for (uint8_t i = 0; i < RAPID_RESTART_MAX; i++) {
if (recentStartTimes[i] == 0 || now - recentStartTimes[i] > RAPID_RESTART_WINDOW_MS) {
rapid = false; break;
}
}
if (rapid) {
state = ST_LATCHED;
logFault(8);
}
}
if (sensor.autoLearnActive) {
unsigned long elapsed = now - sensor.autoLearnStartTime;
if (elapsed >= AUTO_LEARN_STABILIZE_MS && elapsed < AUTO_LEARN_STABILIZE_MS + 10000UL) {
if (enableCurrentSensor && sensor.currentStabilized) {
sensor.learnSumCurrent += sensor.rmsSlowFiltered;
sensor.learnSampleCount++;
}
}
if (elapsed >= AUTO_LEARN_STABILIZE_MS + 10000UL || sensor.learnSampleCount >= AUTO_LEARN_SAMPLES) {
sensor.autoLearnActive = false;
if (sensor.learnSampleCount >= 30) {
float newBaseline = sensor.learnSumCurrent / sensor.learnSampleCount;
if (newBaseline > 0.3f && newBaseline < 12.0f) {
sensor.baselineCurrent = newBaseline;
sensor.runningCurrentAvg = newBaseline;
sensor.lastSavedBaseline = newBaseline;
sensor.learned = true;
noInterrupts();
EEPROM.put(EE_LEARNED_FLAG, (uint8_t)1);
EEPROM.put(EE_LEARNED_BASELINE, newBaseline);
interrupts();
sensor.learnDisplayActive = true;
sensor.learnDisplayTimer = now;
sensor.learnMessageType = 1;
uint8_t seg[4] = {0x5E, 0x3F, 0x54, 0x79};
display.setSegments(seg);
} else {
sensor.learnDisplayActive = true;
sensor.learnDisplayTimer = now;
sensor.learnMessageType = 2;
uint8_t seg[4] = {0x5E, 0x77, 0x77, 0x00};
display.setSegments(seg);
}
} else {
sensor.learnDisplayActive = true;
sensor.learnDisplayTimer = now;
sensor.learnMessageType = 2;
uint8_t seg[4] = {0x5E, 0x77, 0x77, 0x00};
display.setSegments(seg);
}
}
}
if (sensor.learnDisplayActive && now - sensor.learnDisplayTimer >= LEARN_DISPLAY_TIME_MS) {
display.clear();
sensor.learnDisplayActive = false;
}
if (state != prevState) {
if ((state == ST_RUNNING || state == ST_STARTING) &&
!(prevState == ST_RUNNING || prevState == ST_STARTING)) {
sensor.pumpStartTime = now;
sensor.minRunStart = now;
sensor.pumpJustStarted = true;
sensor.currentStabilized = false;
runtime.startCount++;
if (!sensor.learned && sensor.firstRunStartTime == 0) sensor.firstRunStartTime = now;
if (runtime.startCount % 10 == 0) saveEEPROM(true);
else saveEEPROM(false);
}
if ((prevState == ST_RUNNING || prevState == ST_STARTING) &&
!(state == ST_RUNNING || state == ST_STARTING)) {
sensor.lastStopTime = now;
saveEEPROM();
}
if (state == ST_REQUEST) sensor.requestEntryTime = now;
if (state == ST_LATCHED && prevState != ST_LATCHED) {
buzzerStartTime = now;
buzzerLatchedStart = now;
}
prevState = state;
}
}
// ================= READ INPUTS =================
void readInputs() {
unsigned long now = millis();
enableCurrentSensor = (digitalRead(DIP_CURRENT_EN) == LOW);
enableDrySensor = (digitalRead(DIP_DRY_EN) == LOW);
enableVoltageDetection = (digitalRead(DIP_VOLTAGE_EN) == LOW);
bool rTank = (digitalRead(IO_S1_TANK) == LOW);
if (rTank != sensor.tankRaw) { sensor.tankRaw = rTank; sensor.tankDebTime = now; }
if (now - sensor.tankDebTime > 50) sensor.tankDeb = rTank;
sensor.tankNeedsWater = sensor.tankDeb;
bool rWell = (digitalRead(IO_S2_DRY) == LOW);
if (rWell != sensor.wellRaw) { sensor.wellRaw = rWell; sensor.wellDebTime = now; }
if (now - sensor.wellDebTime > 100) sensor.wellDeb = rWell;
if (enableDrySensor) {
sensor.wellHasWater = !sensor.wellDeb;
} else {
sensor.wellHasWater = true;
sensor.wellDeb = false;
sensor.wellRaw = false;
sensor.sourceDryWarningActive = false;
sensor.sourceDryWarningTimer = 0;
}
static bool resetDeb = false;
static unsigned long resetDebTime = 0;
bool resetRaw = (digitalRead(IO_RESET_BTN) == LOW);
if (resetRaw != resetDeb) { resetDebTime = now; resetDeb = resetRaw; }
if (now - resetDebTime > RESET_DEBOUNCE_MS) {
if (resetRaw && !resetPressed) {
resetPressed = true;
resetPressStart = now;
} else if (!resetRaw && resetPressed) {
unsigned long dur = now - resetPressStart;
if (dur < RESET_HOLD_MS && dur > 100) {
buzzerMutedByUser = !buzzerMutedByUser;
if (buzzerMutedByUser) muteStartTime = now;
}
resetPressed = false;
}
}
if (resetPressed) {
unsigned long dur = now - resetPressStart;
if (state == ST_IDLE && dur >= RESET_LONG_MS) {
runtime.hours = 0; runtime.accumulatedMs = 0;
saveEEPROM(true);
resetPressed = false;
} else if (state == ST_RUNNING) {
if (dur >= RESET_FORCE_RELEARN_MS) {
sensor.learned = false; sensor.autoLearnTriggered = false; sensor.firstRunStartTime = 0;
noInterrupts(); EEPROM.update(EE_LEARNED_FLAG, 0); interrupts();
sensor.learnDisplayActive = true;
sensor.learnDisplayTimer = now;
sensor.learnMessageType = 3;
uint8_t seg[4] = {0x38, 0x50, 0x71, 0x54};
display.setSegments(seg);
resetPressed = false;
} else if (dur >= AUTO_LEARN_HOLD_MS) {
performAutoLearn(false);
resetPressed = false;
}
}
}
if (!enableCurrentSensor) {
sensor.calibrated = true; sensor.offset = 2.5f;
} else if (!sensor.calibrated) {
float off = calibrateACS712();
if (off > 0.0f) {
sensor.offset = off; sensor.calibrated = true;
EEPROM.put(EE_ACS_OFFSET, off);
EEPROM.update(EE_OFFSET_VALID, 1);
}
return;
}
if (state == ST_IDLE && now - sensor.lastIdleCalib >= AUTO_CALIB_IDLE_MS && enableCurrentSensor) {
float off = calibrateACS712();
if (off > 0.0f) sensor.offset = off;
sensor.lastIdleCalib = now;
}
float used = INITIAL_NO_LOAD_A;
if (enableCurrentSensor) {
float sumSq = 0.0f;
const uint16_t SAMPLES_PER_CYCLE = 60;
const uint32_t TARGET_US_PER_SAMPLE = 200;
unsigned long sampleStart = micros();
for (uint16_t i = 0; i < SAMPLES_PER_CYCLE; i++) {
int adc = analogRead(IO_CT_CURRENT);
float v = adc * 5.0f / 1023.0f;
float a = (v - sensor.offset) / ACS_SENS;
a = constrain(a, -30.0f, 30.0f);
sumSq += a * a;
unsigned long target = sampleStart + (unsigned long)i * TARGET_US_PER_SAMPLE;
while ((long)(micros() - target) < 0);
}
sensor.rms = sqrt(sumSq / SAMPLES_PER_CYCLE);
sensor.rmsFiltered = 0.6f * sensor.rmsFiltered + 0.4f * sensor.rms;
sensor.rmsSlowFiltered = 0.85f * sensor.rmsSlowFiltered + 0.15f * sensor.rmsFiltered;
used = sensor.rmsSlowFiltered;
if (state == ST_RUNNING && used < 0.6f) used = 0.0f;
} else {
sensor.rms = 0.0f; sensor.rmsFiltered = INITIAL_NO_LOAD_A;
sensor.rmsSlowFiltered = INITIAL_NO_LOAD_A; used = INITIAL_NO_LOAD_A;
sensor.currentStabilized = true;
}
if (enableVoltageDetection) {
sensor.supplyVoltage = analogRead(IO_VOLTAGE) * 5.0f / 1023.0f * VOLTAGE_DIV_RATIO;
sensor.supplyVoltageFiltered = 0.85f * sensor.supplyVoltageFiltered + 0.15f * sensor.supplyVoltage;
} else sensor.supplyVoltageFiltered = 12.5f;
if (enableCurrentSensor) {
float effectiveNoLoad = sensor.learned ? (sensor.baselineCurrent * ADAPTIVE_FACTOR) :
(sensor.currentStabilized ? sensor.runningCurrentAvg * ADAPTIVE_FACTOR : INITIAL_NO_LOAD_A);
bool inNoLoadStartup = (now - sensor.pumpStartTime <= NOLOAD_STARTUP_IGNORE && sensor.pumpJustStarted);
if (used < effectiveNoLoad && !inNoLoadStartup) {
if (sensor.tNoLoadHyst == 0) sensor.tNoLoadHyst = now;
else if (now - sensor.tNoLoadHyst > NOLOAD_HYST_TIME) sensor.noLoad = true;
} else { sensor.noLoad = false; sensor.tNoLoadHyst = 0; }
bool inStartup = (now - sensor.pumpStartTime <= OC_STARTUP_IGNORE && sensor.pumpJustStarted);
if (state == ST_RUNNING && !inStartup) {
float oc_warning_level = 0.0f, oc_trip_level = 0.0f;
if (sensor.learned && sensor.baselineCurrent > 0.4f) {
oc_warning_level = sensor.baselineCurrent * OC_WARNING_FACTOR;
oc_trip_level = sensor.baselineCurrent * OC_TRIP_FACTOR;
} else if (sensor.currentStabilized && sensor.runningCurrentAvg > 0.4f) {
oc_warning_level = sensor.runningCurrentAvg * OC_WARNING_FACTOR;
oc_trip_level = sensor.runningCurrentAvg * OC_TRIP_FACTOR;
} else {
oc_warning_level = 6.80f; oc_trip_level = 8.50f;
}
sensor.overCurrentWarning = (used > oc_warning_level);
if (used > oc_trip_level) {
if (sensor.tOverCurrentHyst == 0) sensor.tOverCurrentHyst = now;
else if (now - sensor.tOverCurrentHyst > OC_HYST_TIME) sensor.overCurrentRaw = true;
} else { sensor.overCurrentRaw = false; sensor.tOverCurrentHyst = 0; }
if (sensor.currentStabilized && used > sensor.runningCurrentAvg * STALL_FACTOR) {
if (sensor.tStallHyst == 0) sensor.tStallHyst = now;
else if (now - sensor.tStallHyst > STALL_HYST_TIME) sensor.stallRaw = true;
} else { sensor.stallRaw = false; sensor.tStallHyst = 0; }
if (now - sensor.pumpStartTime > STABILIZATION_TIME) {
sensor.currentStabilized = true;
sensor.runningCurrentAvg = 0.95f * sensor.runningCurrentAvg + 0.05f * used;
if (sensor.baselineCurrent == 0.0f) sensor.baselineCurrent = sensor.runningCurrentAvg;
if (sensor.learned && sensor.baselineCurrent > 0.4f) {
if (sensor.runningCurrentAvg > sensor.baselineCurrent * AGING_ALLOWANCE) {
float newBase = sensor.baselineCurrent * (1.0f - AGING_SLOW_RATE) + sensor.runningCurrentAvg * AGING_SLOW_RATE;
if (newBase < sensor.baselineCurrent * MAX_BASELINE_INCREASE) {
sensor.baselineCurrent = newBase;
if (millis() - lastEEPROMSave > 300000UL) saveEEPROM(true);
}
}
}
}
float dropThreshold = DRY_DROP_THRESHOLD_BASE, warnThreshold = DRY_WARNING_DROP_BASE;
if (sensor.drySensitivity == 0) { dropThreshold *= 1.25f; warnThreshold *= 1.30f; }
else if (sensor.drySensitivity == 2) { dropThreshold *= 0.80f; warnThreshold *= 0.75f; }
if (sensor.currentStabilized && sensor.runningCurrentAvg > 1.5f && sensor.tankNeedsWater) {
float drop = (sensor.runningCurrentAvg - used) / sensor.runningCurrentAvg;
sensor.currentDropDry = (drop > dropThreshold && used > 1.2f);
sensor.currentDropWarn = (drop > warnThreshold);
if (sensor.currentDropWarn) {
if (sensor.tDropWarn == 0) sensor.tDropWarn = now;
// ================= ADAPTIVE CONFIRMATION WINDOW =================
uint32_t confirmTimeMs;
if (drop > 0.80f) confirmTimeMs = 800UL;
else if (drop > 0.60f) confirmTimeMs = 1500UL;
else if (drop > 0.45f) confirmTimeMs = 2500UL;
else confirmTimeMs = 4000UL;
if (now - sensor.tDropWarn >= confirmTimeMs) {
sensor.currentDropDry = true;
}
} else {
sensor.tDropWarn = 0;
sensor.currentDropWarn = false;
}
} else {
sensor.currentDropDry = false;
sensor.currentDropWarn = false;
sensor.tDropWarn = 0;
}
sensor.healthWarning = (sensor.baselineCurrent > 0 && sensor.runningCurrentAvg > sensor.baselineCurrent * HEALTH_DEGRADE_FACTOR);
} else {
sensor.overCurrentRaw = sensor.stallRaw = sensor.currentDropDry = sensor.currentDropWarn = sensor.healthWarning = false;
sensor.tNoLoadHyst = sensor.tOverCurrentHyst = sensor.tStallHyst = sensor.tDropWarn = 0;
}
} else {
sensor.noLoad = sensor.overCurrentRaw = sensor.stallRaw = sensor.currentDropDry = sensor.currentDropWarn = sensor.healthWarning = false;
sensor.tNoLoadHyst = sensor.tOverCurrentHyst = sensor.tStallHyst = sensor.tDropWarn = 0;
}
}
float calibrateACS712() {
float sum = 0.0f;
const uint8_t samples = 80;
for (uint8_t i = 0; i < samples; i++) {
sum += analogRead(IO_CT_CURRENT) * 5.0f / 1023.0f;
delayMicroseconds(500);
}
float avg = sum / samples;
if (avg > 2.0f && avg < 3.0f) return avg;
return 0.0f;
}
// ================= FSM =================
void fsm() {
unsigned long now = millis();
if ((state == ST_REQUEST || state == ST_STARTING || state == ST_RUNNING) &&
now - sensor.lastStopTime < MIN_OFF_TIME_MS) {
state = ST_IDLE;
return;
}
switch (state) {
case ST_INIT: state = ST_CALIBRATE; break;
case ST_CALIBRATE: if (sensor.calibrated) state = ST_IDLE; break;
case ST_IDLE:
if (sensor.tankNeedsWater && sensor.wellHasWater) state = ST_REQUEST;
else if (sensor.tankNeedsWater && !sensor.wellHasWater && enableDrySensor) {
if (sensor.sourceDryWarningTimer == 0) sensor.sourceDryWarningTimer = now;
if (now - sensor.sourceDryWarningTimer >= SOURCE_DRY_HYST) {
logFault(1); protect.retryDry++; state = ST_DRY_FAULT;
} else sensor.sourceDryWarningActive = true;
} else sensor.sourceDryWarningActive = false;
break;
case ST_REQUEST:
if (now - sensor.requestEntryTime > REQUEST_TIMEOUT || (!sensor.wellHasWater && enableDrySensor)) {
logFault(1); protect.retryDry++; state = ST_DRY_FAULT;
} else { state = ST_STARTING; sensor.startDelayTimer = now; }
break;
case ST_STARTING:
if (now - sensor.startDelayTimer >= START_DELAY_MS) state = ST_RUNNING;
break;
case ST_RUNNING:
if (enableDrySensor && !sensor.wellHasWater) { logFault(1); protect.retryDry++; state = ST_DRY_FAULT; }
else if (enableCurrentSensor && sensor.noLoad) { logFault(2); protect.retryNoLoad++; state = ST_NOLOAD_FAULT; }
else if (enableCurrentSensor && sensor.currentDropDry) { logFault(5); protect.retryDry++; state = ST_DRY_FAULT; }
else if (enableCurrentSensor && sensor.stallRaw) { logFault(4); protect.retryStall++; state = ST_STALL_FAULT; }
else if (enableCurrentSensor && sensor.overCurrentRaw) { logFault(3); protect.retryOC++; state = ST_OVERCURRENT_FAULT; }
else if (!sensor.tankNeedsWater) {
if (now - sensor.minRunStart >= MIN_RUN_TIME_MS) state = ST_IDLE;
}
if (!sensor.learned && sensor.firstRunStartTime > 0 && !sensor.autoLearnActive &&
!sensor.autoLearnTriggered && (now - sensor.firstRunStartTime > AUTO_LEARN_FIRST_MS)) {
performAutoLearn(true); sensor.autoLearnTriggered = true;
}
break;
case ST_DRY_FAULT:
if (sensor.wellHasWater) {
if (sensor.wellRecoveryTimer == 0) sensor.wellRecoveryTimer = now;
else if (now - sensor.wellRecoveryTimer >= WELL_RECOVERY) {
state = (protect.retryDry < MAX_RETRY) ? ST_IDLE : ST_LATCHED;
}
} else sensor.wellRecoveryTimer = 0;
break;
case ST_NOLOAD_FAULT: case ST_OVERCURRENT_FAULT: case ST_STALL_FAULT:
case ST_RELAY_WELD: case ST_BROWNOUT:
bool canRetry = false;
if (state == ST_NOLOAD_FAULT) canRetry = (protect.retryNoLoad < MAX_RETRY);
else if (state == ST_OVERCURRENT_FAULT) canRetry = (protect.retryOC < MAX_RETRY);
else if (state == ST_STALL_FAULT) canRetry = (protect.retryStall < MAX_RETRY);
else if (state == ST_RELAY_WELD) canRetry = (protect.retryWeld < MAX_RETRY);
state = canRetry ? ST_IDLE : ST_LATCHED;
break;
case ST_LATCHED: break;
}
}
// ================= OUTPUTS =================
void outputs() {
bool pumpShouldRun = (state == ST_RUNNING || state == ST_STARTING);
static bool brownoutActive = false;
unsigned long now = millis();
if (enableVoltageDetection) {
bool ignoreBrownout = (state == ST_STARTING || state == ST_RUNNING) &&
(now - sensor.pumpStartTime < BROWNOUT_IGNORE_START_MS);
if (!ignoreBrownout && sensor.supplyVoltageFiltered < (brownoutActive ? BROWNOUT_THRESHOLD : BROWNOUT_THRESHOLD + BROWNOUT_HYSTERESIS)) {
brownoutActive = true; pumpShouldRun = false;
if (state != ST_BROWNOUT) { state = ST_BROWNOUT; logFault(7); }
} else if (sensor.supplyVoltageFiltered > BROWNOUT_THRESHOLD + BROWNOUT_HYSTERESIS) {
brownoutActive = false;
}
}
digitalWrite(IO_R2_PUMP, pumpShouldRun ? HIGH : LOW);
bool dryIssue = sensor.sourceDryWarningActive || (!sensor.wellHasWater && enableDrySensor) ||
(state == ST_DRY_FAULT) || (state == ST_LATCHED);
digitalWrite(IO_R1_DRY, dryIssue ? HIGH : LOW);
}
// ================= RELAY WELD CHECK =================
void checkRelayWeld() {
if (!enableCurrentSensor) { sensor.relayWeldDetected = false; return; }
unsigned long now = millis();
if (digitalRead(IO_R2_PUMP) == LOW) {
if (sensor.rmsSlowFiltered > RELAY_WELD_THRESHOLD) {
if (sensor.relayWeldTimer == 0) sensor.relayWeldTimer = now;
if (now - sensor.relayWeldTimer > WELD_CHECK_TIME) {
sensor.relayWeldDetected = true;
if (state != ST_RELAY_WELD) { state = ST_RELAY_WELD; logFault(6); protect.retryWeld++; }
}
} else sensor.relayWeldTimer = 0;
} else sensor.relayWeldTimer = 0;
}
// ================= BUZZER =================
void updateBuzzer() {
unsigned long now = millis();
if (buzzerMutedByUser) {
if (now - muteStartTime >= MUTE_TIMEOUT_MS) buzzerMutedByUser = false;
else { digitalWrite(IO_BUZZER, LOW); return; }
}
bool isLatched = (state == ST_LATCHED);
bool isFault = (state == ST_DRY_FAULT || state == ST_NOLOAD_FAULT || state == ST_OVERCURRENT_FAULT ||
state == ST_STALL_FAULT || state == ST_RELAY_WELD || state == ST_BROWNOUT || sensor.currentDropDry);
bool isWarning = sensor.sourceDryWarningActive || sensor.currentDropWarn;
if (isLatched) {
if (buzzerLatchedStart == 0) buzzerLatchedStart = now;
if (now - buzzerLatchedStart > BUZZER_MAX_TIME_MS) { digitalWrite(IO_BUZZER, LOW); return; }
unsigned long t = (now - buzzerStartTime) % BUZZER_SOS_PERIOD;
bool on = false;
if (t < 1500) on = (t % 500 < 200);
else if (t < 3500) on = ((t-1500) % 1000 < 600);
else if (t < 5000) on = ((t-3500) % 500 < 200);
digitalWrite(IO_BUZZER, on ? HIGH : LOW);
} else if (isFault) {
if (state == ST_NOLOAD_FAULT) {
unsigned long t = now % BUZZER_NOLOAD_PERIOD;
bool on = (t < 600) && ((t % 200) < 100);
digitalWrite(IO_BUZZER, on ? HIGH : LOW);
} else {
unsigned long t = now % BUZZER_FAST_PERIOD;
digitalWrite(IO_BUZZER, t < BUZZER_FAST_ON ? HIGH : LOW);
}
} else if (isWarning) {
unsigned long t = now % BUZZER_SLOW_PERIOD;
digitalWrite(IO_BUZZER, t < BUZZER_SLOW_ON ? HIGH : LOW);
} else {
digitalWrite(IO_BUZZER, LOW);
buzzerLatchedStart = 0;
}
}
// ================= HMI =================
void updateHMI() {
unsigned long now = millis();
static bool pumpLedState = false;
static unsigned long pumpBlinkTimer = 0;
if (now - pumpBlinkTimer >= (state == ST_LATCHED ? 180 : 500)) {
pumpBlinkTimer = now; pumpLedState = !pumpLedState;
}
digitalWrite(IO_LED_SAFE, (state == ST_IDLE) ? HIGH : LOW);
digitalWrite(IO_LED_PUMP, (state == ST_RUNNING || state == ST_STARTING) ? pumpLedState : LOW);
if (!sensor.wellHasWater || sensor.sourceDryWarningActive) setLedPattern(IO_LED_STATUS, BLINK_SOURCE_DRY);
else if (state == ST_CALIBRATE) setLedPattern(IO_LED_STATUS, BLINK_CALIBRATE);
else if (state == ST_LATCHED) setLedPattern(IO_LED_STATUS, BLINK_LATCHED);
else if (state == ST_DRY_FAULT) setLedPattern(IO_LED_STATUS, BLINK_DRY_RUN);
else if (state == ST_NOLOAD_FAULT || state == ST_OVERCURRENT_FAULT || state == ST_STALL_FAULT ||
state == ST_RELAY_WELD || state == ST_BROWNOUT) setLedPattern(IO_LED_STATUS, BLINK_OTHER_FAULT);
else if (sensor.healthWarning) setLedPattern(IO_LED_STATUS, BLINK_HEALTH_WARN);
else if (state == ST_IDLE && sensor.learned) setLedPattern(IO_LED_STATUS, BLINK_HEALTH_WARN);
else setLedPattern(IO_LED_STATUS, BLINK_OFF);
static unsigned long dispSwitch = 0;
static bool showHours = true;
if (state == ST_RUNNING || state == ST_STARTING) {
if (now - dispSwitch >= DISPLAY_SWITCH_MS) { dispSwitch = now; showHours = !showHours; }
if (showHours) {
uint8_t dots = pumpLedState ? 0x40 : 0x00;
display.showNumberDecEx(min(runtime.hours, 9999UL), dots, false, 4, 0);
} else {
int16_t curr = (int16_t)(sensor.rmsFiltered * 10 + 0.5f);
display.showNumberDec(constrain(curr, 0, 9999), false, 4, 0);
}
} else if (state == ST_IDLE) {
if (!enableCurrentSensor) {
uint8_t seg[4] = {0x39, 0x00, 0x00, 0x00}; display.setSegments(seg);
} else if (sensor.learned) {
uint8_t seg[4] = {0x38, 0x50, 0x71, 0x54}; display.setSegments(seg);
} else display.showNumberDec(min(runtime.hours, 9999UL), false, 4, 0);
} else if (state == ST_CALIBRATE) display.showNumberDecEx(8888, 0xFF, false, 4, 0);
else if (state == ST_DRY_FAULT || state == ST_NOLOAD_FAULT || state == ST_OVERCURRENT_FAULT ||
state == ST_STALL_FAULT || state == ST_RELAY_WELD || state == ST_BROWNOUT || state == ST_LATCHED) {
uint8_t seg[4] = {0,0,0,0};
if (lastFault == 1) { seg[0] = 0x5E; seg[1] = 0x71; }
else if (lastFault == 2) { seg[0] = 0x54; seg[1] = 0x38; }
else if (lastFault == 3) { seg[0] = 0x3F; seg[1] = 0x39; }
else if (lastFault == 4) { seg[0] = 0x6D; seg[1] = 0x71; }
else if (lastFault == 5) { seg[0] = 0x5E; seg[1] = 0x50; }
else if (lastFault == 6) { seg[0] = 0x5E; seg[1] = 0x6D; }
else if (lastFault == 7) { seg[0] = 0x7C; seg[1] = 0x54; }
else if (lastFault == 8) { seg[0] = 0x50; seg[1] = 0x77; seg[2] = 0x39; seg[3] = 0x39; }
display.setSegments(seg);
} else if (sensor.sourceDryWarningActive || sensor.currentDropWarn) {
uint8_t seg[4] = {0x6B, 0x5B, 0x00, 0x00}; display.setSegments(seg);
} else if (sensor.healthWarning) {
uint8_t seg[4] = {0x76, 0x38, 0x73, 0x71}; display.setSegments(seg);
} else display.clear();
}
// ================= HOUR METER =================
void hourMeter() {
if (runtime.accumulatedMs >= HOUR_TICK_MS) {
runtime.hours++;
runtime.accumulatedMs -= HOUR_TICK_MS;
saveEEPROM(true);
}
}