/**
* PROJECT: POMPA_ACS712_PLC_v3.9.8 – FINAL PRODUCTION READY
* Arduino Nano - OEM Pump Controller dengan TM1637 HMI
* Tanggal: Maret 2026
*
* Fitur utama:
* - Dry relay & LED status selalu aktif saat sumur kering (bahkan saat pompa diminta nyala)
* - Mute buzzer otomatis di-reset saat fault baru terjadi
* - Current filtering: 8-sample ring buffer + double EMA
* - Two-level overcurrent: warning 6.8A, trip 8.5A
* - No-load 2.0A, MAX_RETRY = 5
* - Buzzer warning: 2 detik nyala – 8 detik mati
* - TM1637 brightness 0x0A
* - EEPROM save throttle 30 detik
* - Fault display: "dY ", "nL ", "oC "
*/
#include <avr/wdt.h>
#include <EEPROM.h>
#include <math.h>
#include <stdarg.h>
#include <TM1637Display.h>
#define DEBUG_LEVEL 1 // 0 = produksi (minimal log), 1 = error+info
// ================= PINOUT =================
const uint8_t IO_S1_TANK = 2;
const uint8_t IO_S2_DRY = 3;
const uint8_t IO_RESET_BTN = 13;
const uint8_t IO_CT_CURRENT = A3;
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 = 8;
const uint8_t TM_CLK = 11;
const uint8_t TM_DIO = 12;
TM1637Display display(TM_CLK, TM_DIO);
// ================= KONSTANTA =================
const uint32_t SCAN_CYCLE = 50;
const uint32_t WELL_RECOVERY = 10000UL;
const uint32_t REQUEST_TIMEOUT = 10000UL;
const uint32_t NOLOAD_HYST_TIME = 3500UL;
const uint32_t OC_HYST_TIME = 5000UL;
const uint32_t OC_STARTUP_IGNORE = 2500UL;
const float ACS_SENS = 0.066f;
const float NO_LOAD_A = 2.00f;
const float OC_WARNING_A = 6.80f;
const float OVERCURRENT_A = 8.50f;
const uint8_t MAX_RETRY = 5;
const uint32_t HOUR_TICK_MS = 3600000UL;
const uint16_t LOG_THROTTLE_MS = 250;
const uint32_t BUZZER_SINGLE_MS = 5000UL;
const uint32_t BUZZER_REPEAT_MS = 30000UL;
const uint32_t BUZZER_MAX_TIME_MS = 1800000UL;
const uint32_t RESET_BEEP_MS = 100UL;
const uint32_t DISPLAY_SWITCH_MS = 2500UL;
const uint16_t RESET_HOLD_MS = 1000;
const uint16_t RESET_DEBOUNCE_MS = 50;
const uint32_t AUTO_CALIB_IDLE_MS = 1800000UL;
const uint32_t SOURCE_DRY_HYST = 300000UL;
const uint32_t BUZZER_WARN_PERIOD = 10000UL;
const uint32_t BUZZER_WARN_ON = 2000UL;
const uint32_t MIN_EEPROM_SAVE_MS = 30000UL;
const uint8_t TM_BRIGHTNESS = 0x0A;
// ================= EEPROM ADDRESSES =================
const int EE_RUNTIME_SLOTS[4] = {0, 6, 12, 18};
const int EE_FAULT_INDEX = 24;
const int EE_FAULT_LOG = 26; // 5 bytes/entry
const int EE_EEPROM_INDEX = 186;
const int EE_GLOBAL_CRC = 187;
const int EE_ACS_OFFSET = 194;
const int EE_OFFSET_VALID = 198;
// ================= LED PATTERNS =================
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_OFF[] = {0, 0, 0};
// ================= STATES =================
enum PumpState {
ST_INIT, ST_CALIBRATE, ST_IDLE, ST_REQUEST,
ST_RUNNING, ST_DRY_FAULT, ST_NOLOAD_FAULT,
ST_OVERCURRENT_FAULT, ST_LATCHED
};
// ================= DATA STRUCTURES =================
PumpState state = ST_INIT;
PumpState prevState = ST_INIT;
struct {
bool tankRaw, tankDeb, tankNeedsWater;
unsigned long tankDebTime;
bool wellRaw, wellDeb, wellHasWater;
unsigned long wellDebTime;
unsigned long wellRecoveryTimer;
unsigned long requestEntryTime;
float offset;
float currentSamples[8];
uint8_t sampleIndex;
float rms;
float rmsFiltered;
float rmsSlowFiltered;
bool noLoad, overCurrentRaw, overCurrentWarning;
unsigned long tNoLoadHyst, tOverCurrentHyst;
unsigned long pumpStartTime;
bool pumpJustStarted, calibrated;
unsigned long lastIdleCalib;
unsigned long sourceDryWarningTimer;
bool sourceDryWarningActive;
} sensor = {0};
struct { uint8_t retryDry, retryNoLoad, retryOC; } protect = {0};
struct {
uint32_t hours, accumulatedMs;
unsigned long lastTick;
uint8_t faultIndex, eepromIndex;
} runtime = {0};
uint8_t lastFault = 0;
// ================= TIMERS & FLAGS =================
unsigned long lastLogTime = 0;
unsigned long resetPressStart = 0;
bool resetPressed = false;
unsigned long buzzerOnStart = 0;
unsigned long buzzerRepeatTimer= 0;
unsigned long buzzerTotalStart = 0;
unsigned long resetBeepStart = 0;
bool resetBeepActive = false;
unsigned long scanTimer = 0;
unsigned long lastEEPROMSave = 0;
bool alarmMuted = false;
unsigned long muteStartTime = 0;
// ================= LOGGING =================
void logMessage(uint8_t level, const __FlashStringHelper* prefix, const char* format, ...) {
#if DEBUG_LEVEL > 0
if (level > DEBUG_LEVEL) return;
if (millis() - lastLogTime < LOG_THROTTLE_MS && level <= 1) return;
char buf[96];
va_list args;
va_start(args, format);
vsnprintf(buf, sizeof(buf), format, args);
va_end(args);
Serial.print(F("[POMPA] "));
Serial.print(prefix);
Serial.print(' ');
Serial.println(buf);
lastLogTime = millis();
#endif
}
#define LOG_ERROR(fmt, ...) logMessage(1, F("ERROR"), fmt, ##__VA_ARGS__)
#define LOG_INFO(fmt, ...) logMessage(2, F("INFO"), fmt, ##__VA_ARGS__)
// ================= LED CONTROL =================
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 = pin - 5;
if (i > 2) 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);
}
}
// ================= EEPROM FUNCTIONS =================
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[6] = {buf[0],buf[1],buf[2],buf[3], runtime.faultIndex, runtime.eepromIndex};
uint16_t globalCRC = crc16(gbuf, 6);
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_GLOBAL_CRC, globalCRC);
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);
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;
LOG_INFO("ACS offset dari EEPROM: %.3f V", temp);
}
}
}
void logFault(uint8_t type) {
lastFault = type;
uint32_t totalMin = runtime.hours * 60 + (runtime.accumulatedMs / 60000UL);
uint8_t hrsLow = runtime.hours & 0xFF;
uint8_t min = totalMin % 60;
uint8_t data[3] = {type, hrsLow, min};
uint16_t ecrc = crc16(data, 3);
int addr = EE_FAULT_LOG + runtime.faultIndex * 5;
noInterrupts();
EEPROM.update(addr, type);
EEPROM.update(addr + 1, hrsLow);
EEPROM.update(addr + 2, min);
EEPROM.put(addr + 3, ecrc);
interrupts();
runtime.faultIndex = (runtime.faultIndex + 1) % 32;
saveEEPROM(true);
const char* nm = (type==1)?"DRY":(type==2)?"NOLOAD":"OC";
LOG_ERROR("FAULT %s @ %lu h %02u min", nm, runtime.hours, min);
// Reset mute saat fault baru
if (alarmMuted) {
alarmMuted = false;
LOG_INFO("Mute direset karena fault baru (%s)", nm);
}
}
// ================= SETUP =================
void setup() {
wdt_disable();
Serial.begin(115200);
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_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);
// Power-on self-test
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();
runtime.lastTick = millis();
if (!sensor.calibrated) sensor.offset = 2.5f;
sensor.rmsFiltered = 3.0f;
sensor.rmsSlowFiltered = 3.0f;
sensor.lastIdleCalib = millis();
wdt_enable(WDTO_1S);
LOG_INFO("Started v3.9.8 FINAL PRODUCTION READY");
}
// ================= MAIN LOOP =================
void loop() {
wdt_reset();
unsigned long now = millis();
if (now - scanTimer >= SCAN_CYCLE) {
scanTimer = now;
plcCycle();
updateHMI();
}
}
// ================= PLC CYCLE =================
void plcCycle() {
unsigned long now = millis();
bool wasRunning = (prevState == ST_RUNNING);
if (state != prevState) {
if (state == ST_RUNNING) {
sensor.pumpStartTime = now;
sensor.pumpJustStarted = true;
sensor.noLoad = false;
sensor.tNoLoadHyst = 0;
sensor.overCurrentRaw = false;
sensor.tOverCurrentHyst = 0;
sensor.sourceDryWarningTimer = 0;
sensor.sourceDryWarningActive = false;
}
if (state == ST_REQUEST) sensor.requestEntryTime = now;
if (wasRunning && state != ST_RUNNING) {
runtime.accumulatedMs += (now - runtime.lastTick);
runtime.lastTick = now;
saveEEPROM();
sensor.pumpJustStarted = false;
}
prevState = state;
}
readInputs();
fsm();
outputs();
hourMeter();
}
// ================= READ INPUTS =================
void readInputs() {
unsigned long now = millis();
// Tank sensor
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;
// Well sensor
bool rWell = (digitalRead(IO_S2_DRY) == LOW);
if (rWell != sensor.wellRaw) {
sensor.wellRaw = rWell;
sensor.wellDebTime = now;
}
if (now - sensor.wellDebTime > 50) sensor.wellDeb = rWell;
sensor.wellHasWater = !sensor.wellDeb;
// Reset button logic
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) {
// Short press: mute
if (sensor.sourceDryWarningActive || state == ST_LATCHED) {
alarmMuted = true;
muteStartTime = now;
digitalWrite(IO_BUZZER, LOW);
LOG_INFO("Buzzer dimute (short press)");
}
}
resetPressed = false;
}
}
// Long press: full reset latch
if (resetPressed && state == ST_LATCHED && now - resetPressStart >= RESET_HOLD_MS) {
protect.retryDry = protect.retryNoLoad = protect.retryOC = 0;
lastFault = 0;
alarmMuted = false;
digitalWrite(IO_BUZZER, HIGH);
resetBeepStart = now;
resetBeepActive = true;
state = ST_IDLE;
LOG_INFO("Full reset latch (long press)");
resetPressed = false;
}
if (resetBeepActive && now - resetBeepStart >= RESET_BEEP_MS) {
digitalWrite(IO_BUZZER, LOW);
resetBeepActive = false;
}
// Calibration
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);
LOG_INFO("Kalibrasi selesai: %.3f V", off);
}
return;
}
// Auto-recalibration
if (state == ST_IDLE && now - sensor.lastIdleCalib >= AUTO_CALIB_IDLE_MS) {
float off = calibrateACS712();
if (off > 0.0f) {
sensor.offset = off;
EEPROM.put(EE_ACS_OFFSET, off);
LOG_INFO("Auto-rekalibrasi: %.3f V", off);
}
sensor.lastIdleCalib = now;
}
// Current measurement
int adc = analogRead(IO_CT_CURRENT);
float v = (adc * 5.0f) / 1024.0f;
float a = (v - sensor.offset) / ACS_SENS;
if (a > 30.0f) a = 30.0f;
if (a < -30.0f) a = -30.0f;
sensor.currentSamples[sensor.sampleIndex] = a;
sensor.sampleIndex = (sensor.sampleIndex + 1) % 8;
float sumSq = 0.0f;
for (uint8_t i = 0; i < 8; i++) sumSq += sensor.currentSamples[i] * sensor.currentSamples[i];
sensor.rms = sqrt(sumSq / 8.0f);
sensor.rmsFiltered = 0.6f * sensor.rmsFiltered + 0.4f * sensor.rms;
sensor.rmsSlowFiltered = 0.85f * sensor.rmsSlowFiltered + 0.15f * sensor.rmsFiltered;
float used = sensor.rmsSlowFiltered;
// No-load
if (used < NO_LOAD_A) {
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;
}
// Overcurrent
bool inStartup = (now - sensor.pumpStartTime <= OC_STARTUP_IGNORE && sensor.pumpJustStarted);
if (state == ST_RUNNING && !inStartup) {
sensor.overCurrentWarning = (used > OC_WARNING_A);
if (used > OVERCURRENT_A) {
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;
}
} else {
sensor.overCurrentWarning = false;
sensor.overCurrentRaw = false;
sensor.tOverCurrentHyst = 0;
}
}
// ================= CALIBRATION =================
float calibrateACS712() {
static unsigned long start = 0;
static uint16_t count = 0;
static float sum = 0.0f;
unsigned long now = millis();
if (start == 0) {
start = now;
count = 0;
sum = 0.0f;
return 0.0f;
}
if (now - start > 10000UL) {
LOG_ERROR("Kalibrasi timeout");
start = 0;
return 2.5f;
}
sum += (analogRead(IO_CT_CURRENT) * 5.0f) / 1024.0f;
count++;
if (count >= 500) {
float off = sum / 500.0f;
start = count = 0;
sum = 0.0f;
return (off >= 2.0f && off <= 3.0f) ? off : 2.5f;
}
return 0.0f;
}
// ================= FSM =================
void fsm() {
unsigned long now = millis();
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;
sensor.sourceDryWarningTimer = 0;
sensor.sourceDryWarningActive = false;
}
else if (!sensor.tankNeedsWater && !sensor.wellHasWater) {
if (sensor.sourceDryWarningTimer == 0) sensor.sourceDryWarningTimer = now;
if (now - sensor.sourceDryWarningTimer >= SOURCE_DRY_HYST) {
logFault(1);
protect.retryDry++;
state = ST_DRY_FAULT;
sensor.sourceDryWarningTimer = 0;
sensor.sourceDryWarningActive = false;
} else {
sensor.sourceDryWarningActive = true;
}
} else {
sensor.sourceDryWarningTimer = 0;
sensor.sourceDryWarningActive = false;
}
break;
case ST_REQUEST:
if (now - sensor.requestEntryTime > REQUEST_TIMEOUT) {
LOG_ERROR("Request timeout → dry fault");
logFault(1);
protect.retryDry++;
state = ST_DRY_FAULT;
break;
}
if (!sensor.wellHasWater) {
logFault(1);
protect.retryDry++;
state = ST_DRY_FAULT;
}
else if (sensor.noLoad) {
logFault(2);
protect.retryNoLoad++;
state = ST_NOLOAD_FAULT;
}
else if (sensor.overCurrentRaw) {
logFault(3);
protect.retryOC++;
state = ST_OVERCURRENT_FAULT;
}
else {
state = ST_RUNNING;
}
break;
case ST_RUNNING:
if (sensor.pumpJustStarted && now - sensor.pumpStartTime > OC_STARTUP_IGNORE)
sensor.pumpJustStarted = false;
if (!sensor.wellHasWater) {
logFault(1);
protect.retryDry++;
state = ST_DRY_FAULT;
}
else if (sensor.noLoad) {
logFault(2);
protect.retryNoLoad++;
state = ST_NOLOAD_FAULT;
}
else if (sensor.overCurrentRaw) {
logFault(3);
protect.retryOC++;
state = ST_OVERCURRENT_FAULT;
}
else if (!sensor.tankNeedsWater) {
state = ST_IDLE;
}
break;
case ST_DRY_FAULT:
if (sensor.wellHasWater) {
if (sensor.wellRecoveryTimer == 0) sensor.wellRecoveryTimer = now;
else if (now - sensor.wellRecoveryTimer >= WELL_RECOVERY) {
if (protect.retryDry < MAX_RETRY) state = ST_IDLE;
else state = ST_LATCHED;
}
} else {
sensor.wellRecoveryTimer = 0;
}
sensor.sourceDryWarningTimer = 0;
sensor.sourceDryWarningActive = false;
break;
case ST_NOLOAD_FAULT:
case ST_OVERCURRENT_FAULT:
bool canRetry = (state == ST_NOLOAD_FAULT && protect.retryNoLoad < MAX_RETRY) ||
(state == ST_OVERCURRENT_FAULT && protect.retryOC < MAX_RETRY);
state = canRetry ? ST_IDLE : ST_LATCHED;
break;
case ST_LATCHED:
break;
}
}
// ================= OUTPUTS =================
void outputs() {
digitalWrite(IO_R2_PUMP, (state == ST_RUNNING) ? HIGH : LOW);
bool dryIssue = sensor.sourceDryWarningActive ||
!sensor.wellHasWater ||
(state == ST_DRY_FAULT) ||
(state == ST_LATCHED);
digitalWrite(IO_R1_DRY, dryIssue ? HIGH : LOW);
if (!sensor.wellHasWater || sensor.sourceDryWarningActive) {
setLedPattern(IO_LED_STATUS, BLINK_SOURCE_DRY);
}
else if (state == ST_CALIBRATE) {
setLedPattern(IO_LED_STATUS, BLINK_OFF);
setLedPattern(IO_LED_SAFE, 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)
setLedPattern(IO_LED_STATUS, BLINK_OTHER_FAULT);
else setLedPattern(IO_LED_STATUS, BLINK_OFF);
}
// ================= HMI =================
void updateHMI() {
unsigned long now = millis();
static bool pumpLedState = false;
static unsigned long pumpBlinkTimer = 0;
uint16_t interval = (state == ST_LATCHED) ? 180 : 500;
if (now - pumpBlinkTimer >= interval) {
pumpBlinkTimer = now;
pumpLedState = !pumpLedState;
}
digitalWrite(IO_LED_SAFE, (state == ST_IDLE) ? HIGH : LOW);
digitalWrite(IO_LED_PUMP, (state == ST_RUNNING) ? pumpLedState : LOW);
// Buzzer latched
bool latchedBuzz = (state == ST_LATCHED && pumpLedState && !alarmMuted);
if (latchedBuzz) {
if (buzzerTotalStart == 0) buzzerTotalStart = now;
if (now - buzzerTotalStart < BUZZER_MAX_TIME_MS) {
if (buzzerOnStart == 0 || now - buzzerRepeatTimer >= BUZZER_REPEAT_MS) {
buzzerOnStart = now;
buzzerRepeatTimer = now;
}
digitalWrite(IO_BUZZER, (now - buzzerOnStart < BUZZER_SINGLE_MS) ? HIGH : LOW);
} else {
digitalWrite(IO_BUZZER, LOW);
}
} else if (state == ST_LATCHED) {
digitalWrite(IO_BUZZER, LOW);
buzzerOnStart = buzzerRepeatTimer = buzzerTotalStart = 0;
} else {
digitalWrite(IO_BUZZER, LOW);
buzzerOnStart = buzzerRepeatTimer = buzzerTotalStart = 0;
}
// Warning buzzer
bool warnBuzz = sensor.sourceDryWarningActive &&
((now % BUZZER_WARN_PERIOD) < BUZZER_WARN_ON) &&
!alarmMuted;
if (warnBuzz && !latchedBuzz) digitalWrite(IO_BUZZER, HIGH);
else if (sensor.sourceDryWarningActive && !latchedBuzz) digitalWrite(IO_BUZZER, LOW);
// Display
static unsigned long dispSwitch = 0;
static bool showHours = true;
if (state == ST_RUNNING) {
if (now - dispSwitch >= DISPLAY_SWITCH_MS) {
dispSwitch = now;
showHours = !showHours;
}
if (showHours) {
uint32_t hrs = min(runtime.hours, 9999UL);
uint8_t dots = pumpLedState ? 0x40 : 0x00;
display.showNumberDecEx(hrs, dots, false, 4, 0);
} else {
int16_t curr = (int16_t)(sensor.rmsFiltered * 10.0f + 0.5f);
curr = constrain(curr, 0, 9999);
display.showNumberDec(curr, false, 4, 0);
}
}
else if (state == ST_IDLE) {
uint32_t hrs = min(runtime.hours, 9999UL);
display.showNumberDec(hrs, 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_LATCHED) {
uint8_t seg[4] = {0,0,0,0};
if (lastFault == 1) { seg[0] = 0x5E; seg[1] = 0x71; } // dY
else if (lastFault == 2) { seg[0] = 0x54; seg[1] = 0x38; } // nL
else { seg[0] = 0x3F; seg[1] = 0x39; } // oC
display.setSegments(seg);
}
else if (sensor.sourceDryWarningActive) {
uint8_t seg[4] = {0x6B, 0x5B, 0x00, 0x00}; // Sd
display.setSegments(seg);
}
else {
display.clear();
}
}
// ================= HOUR METER =================
void hourMeter() {
unsigned long now = millis();
if (state == ST_RUNNING) {
runtime.accumulatedMs += (now - runtime.lastTick);
runtime.lastTick = now;
if (runtime.accumulatedMs >= HOUR_TICK_MS) {
runtime.hours++;
runtime.accumulatedMs -= HOUR_TICK_MS;
saveEEPROM(true);
}
} else {
runtime.lastTick = now;
}
}