/**
* PROJECT: POMPA_ACS712_PLC_v3.7.0 – FINAL CLEAN & PRODUCTION READY
* Arduino Nano - OEM Pump Controller dengan TM1637 HMI
*
* v3.7.0 – Revisi besar sesuai saran:
* • Nama variabel jelas (tankHasWater, wellHasWater)
* • Offset ACS712 disimpan di EEPROM
* • LED_SAFE solid ON saat selesai kalibrasi
* • Power-on self-test lengkap
* • HMI bergantian jam ↔ arus saat running
* • Request timeout 10 detik, save setiap jam, dll
*/
#include <avr/wdt.h>
#include <EEPROM.h>
#include <math.h>
#include <stdarg.h>
#include <TM1637Display.h>
#define DEBUG_LEVEL 2 // 0 = produksi, 2 = debug
/* ================= PIN ================= */
const uint8_t IO_S1_TANK = 2; // LOW = ada air
const uint8_t IO_S2_DRY = 3; // LOW = ada air (tidak kering)
const uint8_t IO_RESET_BTN = 13;
const uint8_t IO_CT_CURRENT = A3;
const uint8_t IO_R1_DRY = 9; // ACTIVE LOW saat kering
const uint8_t IO_R2_PUMP = 10; // ACTIVE HIGH saat running
const uint8_t IO_LED_DRY = 4;
const uint8_t IO_LED_PUMP = 5;
const uint8_t IO_LED_SAFE = 6;
const uint8_t IO_LED_FAULT = 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 HYST_SENSOR = 2000;
const uint32_t WELL_RECOVERY = 10000;
const uint32_t REQUEST_TIMEOUT = 10000;
const uint32_t CURRENT_HYST = 5000;
const uint32_t OC_STARTUP_IGNORE = 2000;
const uint32_t OC_CONFIRM_TIME = 5000;
const float ACS_SENS = 0.066;
const float NO_LOAD_A = 2.5;
const float OVERCURRENT_A = 8.0;
const uint8_t MAX_RETRY = 3;
const uint32_t HOUR_TICK_MS = 3600000UL;
const uint16_t LOG_THROTTLE_MS = 250;
const uint32_t BUZZER_MAX_ON_MS = 30000;
const uint32_t RESET_BEEP_MS = 100;
const uint32_t DISPLAY_SWITCH_MS = 2500;
/* ================= EEPROM ================= */
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 = 122;
const int EE_GLOBAL_CRC = 123;
const int EE_ACS_OFFSET = 130; // float 4 byte
const int EE_OFFSET_VALID = 134; // uint8_t 0/1
/* ================= FSM ================= */
enum PumpState {
ST_INIT, ST_CALIBRATE, ST_IDLE, ST_REQUEST,
ST_RUNNING, ST_DRY_FAULT, ST_NOLOAD_FAULT,
ST_OVERCURRENT_FAULT, ST_LATCHED
};
PumpState state = ST_INIT;
PumpState prevState = ST_INIT;
/* ================= DATA ================= */
struct {
// Sensor level
bool tankRaw, tankDeb, tankHasWater;
unsigned long tankDebTime, tankHystTime;
bool wellRaw, wellDeb, wellHasWater;
unsigned long wellDebTime, wellHystTime;
unsigned long wellRecoveryTimer;
unsigned long requestEntryTime;
// Current
float offset, rms, rmsFiltered, sumSq;
uint16_t samples;
bool noLoad, overCurrentRaw;
unsigned long tCurrentHyst, tOverCurrentHyst;
unsigned long pumpStartTime;
bool pumpJustStarted, calibrated;
} sensor = {0};
struct {
uint8_t retryDry, retryNoLoad, retryOC;
} protect = {0};
struct {
uint32_t hours;
uint32_t accumulatedMs;
unsigned long lastTick;
uint8_t faultIndex;
uint8_t eepromIndex;
} runtime = {0};
uint8_t lastFault = 0;
/* ================= VARIABEL ================= */
unsigned long lastLogTime = 0;
unsigned long lastResetPress = 0;
unsigned long buzzerOnStart = 0;
unsigned long resetBeepStart = 0;
bool resetBeepActive = false;
const uint16_t RESET_DEBOUNCE = 50;
unsigned long scanTimer = 0;
/* ================= LOGGING ================= */
void logMessage(uint8_t level, const __FlashStringHelper* prefix, const char* format, ...) {
#if DEBUG_LEVEL > 0
if (DEBUG_LEVEL < level) return;
if (level <= 2 && millis() - lastLogTime < LOG_THROTTLE_MS) 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(F(" "));
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__)
#define LOG_DEBUG(fmt, ...) logMessage(3, F("DEBUG"), fmt, ##__VA_ARGS__)
/* ================= CRC16 ================= */
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;
}
/* ================= EEPROM ================= */
void saveEEPROM(bool force = false) {
unsigned long now = millis();
static unsigned long lastSave = 0;
if (!force && now - lastSave < 600000UL) return;
runtime.eepromIndex = (runtime.eepromIndex + 1) % 4;
int slotAddr = 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(slotAddr + i, buf[i]);
EEPROM.put(slotAddr + 4, slotCRC);
EEPROM.update(EE_FAULT_INDEX, runtime.faultIndex);
EEPROM.update(EE_EEPROM_INDEX, runtime.eepromIndex);
EEPROM.put(EE_GLOBAL_CRC, globalCRC);
interrupts();
lastSave = now;
}
void loadEEPROM() {
EEPROM.get(EE_EEPROM_INDEX, runtime.eepromIndex);
if (runtime.eepromIndex > 3) runtime.eepromIndex = 0;
uint32_t bestHours = 0;
uint8_t bestSlot = 0;
for (uint8_t slot = 0; slot < 4; slot++) {
int addr = EE_RUNTIME_SLOTS[slot];
uint32_t h = 0;
uint16_t c = 0;
for (uint8_t i = 0; i < 4; i++) h |= ((uint32_t)EEPROM.read(addr + i) << (i * 8));
EEPROM.get(addr + 4, c);
uint8_t buf[4];
for (uint8_t i = 0; i < 4; i++) buf[i] = (h >> (i*8)) & 0xFF;
if (crc16(buf, 4) == c && h > bestHours) {
bestHours = h;
bestSlot = slot;
}
}
runtime.hours = bestHours;
runtime.eepromIndex = bestSlot;
runtime.accumulatedMs = 0;
EEPROM.get(EE_FAULT_INDEX, runtime.faultIndex);
// Load ACS offset
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("Offset ACS712 dari EEPROM: %.3f V", temp);
}
}
}
/* ================= FAULT ================= */
void logFault(uint8_t type) {
lastFault = type;
int addr = EE_FAULT_LOG + runtime.faultIndex * 3;
EEPROM.update(addr, type);
EEPROM.update(addr + 1, runtime.hours & 0xFF);
EEPROM.update(addr + 2, (runtime.hours >> 8) & 0xFF);
runtime.faultIndex = (runtime.faultIndex + 1) % 32;
saveEEPROM(true);
const char* nama = (type == 1) ? "DRY RUN" : (type == 2) ? "NO LOAD" : "OVER CURRENT";
LOG_ERROR("FAULT: %s @ %lu jam", nama, runtime.hours);
}
/* ================= SETUP ================= */
void setup() {
wdt_disable();
Serial.begin(115200);
display.setBrightness(0x0f);
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_DRY, OUTPUT);
pinMode(IO_LED_PUMP, OUTPUT);
pinMode(IO_LED_SAFE, OUTPUT);
pinMode(IO_LED_FAULT, OUTPUT);
pinMode(IO_BUZZER, OUTPUT);
// Power-on Self-Test
digitalWrite(IO_LED_SAFE, HIGH); digitalWrite(IO_LED_PUMP, HIGH);
digitalWrite(IO_LED_DRY, HIGH); digitalWrite(IO_LED_FAULT, HIGH);
digitalWrite(IO_BUZZER, HIGH);
delay(300);
digitalWrite(IO_LED_SAFE, LOW); digitalWrite(IO_LED_PUMP, LOW);
digitalWrite(IO_LED_DRY, LOW); digitalWrite(IO_LED_FAULT, LOW);
digitalWrite(IO_BUZZER, LOW);
delay(200);
// Test relay singkat
digitalWrite(IO_R1_DRY, LOW); delay(100); digitalWrite(IO_R1_DRY, HIGH);
digitalWrite(IO_R2_PUMP, HIGH); delay(100); digitalWrite(IO_R2_PUMP, LOW);
digitalWrite(IO_R1_DRY, HIGH);
digitalWrite(IO_R2_PUMP, LOW);
digitalWrite(IO_LED_SAFE, HIGH);
digitalWrite(IO_LED_PUMP, LOW);
digitalWrite(IO_LED_DRY, LOW);
digitalWrite(IO_LED_FAULT, LOW);
digitalWrite(IO_BUZZER, LOW);
loadEEPROM();
runtime.lastTick = millis();
sensor.calibrated = sensor.calibrated; // sudah di-set di loadEEPROM
if (!sensor.calibrated) sensor.offset = 2.5f;
sensor.rmsFiltered = 3.0f;
sensor.sumSq = 0.0f;
wdt_enable(WDTO_1S);
#if DEBUG_LEVEL > 0
LOG_INFO("Sistem start – v3.7.0 FINAL REVISI");
#endif
}
/* ================= LOOP ================= */
void loop() {
wdt_reset();
unsigned long now = millis();
if (now - scanTimer >= SCAN_CYCLE) {
scanTimer = now;
plc();
updateHMI();
}
}
/* ================= PLC ================= */
void plc() {
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.tCurrentHyst = 0;
sensor.overCurrentRaw = false;
sensor.tOverCurrentHyst = 0;
}
if (state == ST_REQUEST) {
sensor.requestEntryTime = now;
}
if (wasRunning && state != ST_RUNNING) {
runtime.accumulatedMs += (now - runtime.lastTick);
runtime.lastTick = now;
saveEEPROM(true);
sensor.pumpJustStarted = false;
}
prevState = state;
}
readInputs();
fsm();
outputs();
hourMeter();
}
/* ================= READ INPUTS ================= */
void readInputs() {
unsigned long now = millis();
// S1 - Tangki
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;
if (sensor.tankDeb) {
if (sensor.tankHystTime == 0) sensor.tankHystTime = now;
if (now - sensor.tankHystTime > HYST_SENSOR) sensor.tankHasWater = true;
} else {
sensor.tankHasWater = false;
sensor.tankHystTime = 0;
}
// S2 - Sumur (well)
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;
if (sensor.wellDeb) {
if (sensor.wellHystTime == 0) sensor.wellHystTime = now;
if (now - sensor.wellHystTime > HYST_SENSOR) sensor.wellHasWater = true;
} else {
sensor.wellHasWater = false;
sensor.wellHystTime = 0;
}
#if DEBUG_LEVEL > 0
LOG_DEBUG("Tank=%d Well=%d | state=%d", sensor.tankHasWater, sensor.wellHasWater, state);
#endif
// Reset button
if (!digitalRead(IO_RESET_BTN) && state == ST_LATCHED) {
if (millis() - lastResetPress > RESET_DEBOUNCE) {
protect.retryDry = protect.retryNoLoad = protect.retryOC = 0;
lastFault = 0;
display.clear();
digitalWrite(IO_LED_FAULT, HIGH);
digitalWrite(IO_BUZZER, HIGH);
resetBeepStart = millis();
resetBeepActive = true;
state = ST_IDLE;
lastResetPress = millis();
LOG_INFO("RESET dikonfirmasi");
}
}
if (resetBeepActive && millis() - resetBeepStart >= RESET_BEEP_MS) {
digitalWrite(IO_BUZZER, LOW);
digitalWrite(IO_LED_FAULT, LOW);
resetBeepActive = false;
display.showNumberDec(runtime.hours % 10000, false, 4, 0);
}
if (!sensor.calibrated) {
float tempOffset = calibrateACS712();
if (tempOffset > 0.0f) {
sensor.offset = tempOffset;
sensor.calibrated = true;
EEPROM.put(EE_ACS_OFFSET, tempOffset);
EEPROM.update(EE_OFFSET_VALID, 1);
LOG_INFO("Kalibrasi selesai & tersimpan – Offset: %.3f V", tempOffset);
}
return;
}
// Current measurement (sama seperti sebelumnya)
int adc = analogRead(IO_CT_CURRENT);
float v = (adc * 5.0) / 1024.0;
float a = (v - sensor.offset) / ACS_SENS;
sensor.sumSq += a * a;
sensor.samples++;
if (sensor.samples >= 100) {
sensor.rms = sqrt(sensor.sumSq / 100.0f);
sensor.sumSq = 0.0f;
sensor.samples = 0;
sensor.rmsFiltered = sensor.rmsFiltered * 0.82f + sensor.rms * 0.18f;
if (sensor.rmsFiltered < NO_LOAD_A) {
if (sensor.tCurrentHyst == 0) sensor.tCurrentHyst = now;
else if (now - sensor.tCurrentHyst > CURRENT_HYST) sensor.noLoad = true;
} else {
sensor.noLoad = false;
sensor.tCurrentHyst = 0;
}
if (sensor.rmsFiltered > OVERCURRENT_A && state == ST_RUNNING && !sensor.pumpJustStarted) {
if (sensor.tOverCurrentHyst == 0) sensor.tOverCurrentHyst = now;
else if (now - sensor.tOverCurrentHyst > OC_CONFIRM_TIME) sensor.overCurrentRaw = true;
} else {
sensor.overCurrentRaw = false;
sensor.tOverCurrentHyst = 0;
}
}
}
/* ================= KALIBRASI ================= */
float calibrateACS712() {
static uint16_t count = 0;
static float sum = 0.0f;
static unsigned long start = 0;
static unsigned long timeout = 0;
unsigned long now = millis();
if (start == 0) {
start = now; timeout = now + 10000UL; count = 0; sum = 0.0f;
return 0.0f;
}
if (now > timeout) {
LOG_ERROR("Kalibrasi timeout – pakai default 2.5 V");
start = count = 0; sum = 0.0f;
return 2.5f;
}
sum += analogRead(IO_CT_CURRENT) * 5.0f / 1024.0f;
count++;
if (count >= 500) {
float offset = sum / 500.0f;
start = count = 0; sum = 0.0f;
return (offset >= 2.0f && offset <= 3.0f) ? offset : 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.tankHasWater && sensor.wellHasWater) state = ST_REQUEST;
break;
case ST_REQUEST:
if (now - sensor.requestEntryTime > REQUEST_TIMEOUT) {
LOG_ERROR("Request timeout → force 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.tankHasWater) 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;
}
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;
}
}
/* ================= OUTPUTS ================= */
void outputs() {
digitalWrite(IO_R2_PUMP, (state == ST_RUNNING) ? HIGH : LOW);
bool isDry = (state == ST_DRY_FAULT || state == ST_LATCHED);
digitalWrite(IO_R1_DRY, isDry ? LOW : HIGH);
digitalWrite(IO_LED_DRY, isDry ? HIGH : LOW);
}
/* ================= UPDATE HMI ================= */
void updateHMI() {
unsigned long now = millis();
static unsigned long ledBlinkTimer = 0;
static bool ledBlinkState = false;
uint16_t interval = (state == ST_LATCHED) ? 180 : 500;
if (now - ledBlinkTimer >= interval) {
ledBlinkTimer = now;
ledBlinkState = !ledBlinkState;
}
// LED SAFE: solid ON di IDLE (setelah kalibrasi)
digitalWrite(IO_LED_SAFE, (state == ST_IDLE) ? HIGH :
(state == ST_CALIBRATE ? ledBlinkState : LOW));
digitalWrite(IO_LED_PUMP, (state == ST_RUNNING) && ledBlinkState);
digitalWrite(IO_LED_DRY, (state == ST_DRY_FAULT) && ledBlinkState);
digitalWrite(IO_LED_FAULT, (state == ST_LATCHED));
// Buzzer latch
bool shouldBuzz = (state == ST_LATCHED && ledBlinkState);
if (shouldBuzz) {
if (buzzerOnStart == 0) buzzerOnStart = now;
digitalWrite(IO_BUZZER, (now - buzzerOnStart < BUZZER_MAX_ON_MS) ? HIGH : LOW);
} else {
digitalWrite(IO_BUZZER, LOW);
buzzerOnStart = 0;
}
// Display
static unsigned long displaySwitchTimer = 0;
static bool showHours = true;
if (state == ST_RUNNING) {
if (now - displaySwitchTimer >= DISPLAY_SWITCH_MS) {
displaySwitchTimer = now;
showHours = !showHours;
}
if (showHours) {
uint8_t dots = ledBlinkState ? 0x40 : 0x00;
display.showNumberDecEx(runtime.hours % 10000, dots, false, 4, 0);
} else {
int16_t curr = (int16_t)(sensor.rmsFiltered * 10.0f + 0.5f);
if (curr < 0) curr = 0;
if (curr > 9999) curr = 9999;
display.showNumberDec(curr, false, 4, 0); // contoh: 052 = 5.2 A
}
}
else if (state == ST_IDLE || state == ST_CALIBRATE) {
display.showNumberDec(runtime.hours % 10000, false, 4, 0);
}
else if (state == ST_DRY_FAULT || state == ST_NOLOAD_FAULT ||
state == ST_OVERCURRENT_FAULT || state == ST_LATCHED) {
uint8_t code = (lastFault == 1) ? 1 : (lastFault == 2) ? 2 : 3;
uint8_t data[] = {0x71, display.encodeDigit(0), display.encodeDigit(code/10), display.encodeDigit(code%10)};
display.setSegments(data);
}
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); // force save setiap jam
}
} else {
runtime.lastTick = now;
}
}