/*
* SLIDING KANOPI OTOMATIS V5.28 – PRODUCTION LOCKED + BUGFIX
* ============================================================
* Tanggal: 03 April 2026
* Status: FINAL – Siap produksi massal (setelah perbaikan kritis)
* Perbaikan QA:
* - LowPower + WDT race condition → FIXED
* - RTC alarm handling → FIXED
* - Stall detection → FIXED (170% + 4000ms)
* - Enum declaration order → FIXED (compile clean di DEBUG_LEVEL > 0)
*/
#include <avr/wdt.h>
#include <Wire.h>
#include <RTClib.h>
#include <LowPower.h>
#include <EEPROM.h>
#include <stdarg.h>
// ================= ENUM MOTOR STATE (DIPINDAHKAN KE PALING ATAS) =================
enum MotorState { IDLE, START_CW, RUNNING_CW, START_CCW, RUNNING_CCW, STOPPING, COOLDOWN, ERROR };
// ================= KONFIGURASI DEBUG LEVEL =================
#ifndef DEBUG_LEVEL
#define DEBUG_LEVEL 2 // 0 = PRODUKSI (logging & Serial dimatikan total)
#endif
// ================= MACRO LOGGING (hanya aktif kalau DEBUG_LEVEL > 0) =================
#if DEBUG_LEVEL > 0
#define LOG_ERROR(id, msg) log_throttled(id, 0, msg)
#define LOG_ERRORF(id, fmt, ...) log_throttled_f(id, 0, fmt, ##__VA_ARGS__)
#define LOG_WARN(id, msg) log_throttled(id, 1, msg)
#define LOG_WARNF(id, fmt, ...) log_throttled_f(id, 1, fmt, ##__VA_ARGS__)
#define LOG_INFO(id, msg) log_throttled(id, 2, msg)
#define LOG_INFOF(id, fmt, ...) log_throttled_f(id, 2, fmt, ##__VA_ARGS__)
#define LOG_DEBUG(id, msg) log_throttled(id, 3, msg)
#define LOG_DEBUGF(id, fmt, ...) log_throttled_f(id, 3, fmt, ##__VA_ARGS__)
#else
#define LOG_ERROR(id, msg)
#define LOG_ERRORF(id, fmt, ...)
#define LOG_WARN(id, msg)
#define LOG_WARNF(id, fmt, ...)
#define LOG_INFO(id, msg)
#define LOG_INFOF(id, fmt, ...)
#define LOG_DEBUG(id, msg)
#define LOG_DEBUGF(id, fmt, ...)
#endif
#if DEBUG_LEVEL > 0
static unsigned long lastLogTimeGlobal = 0;
static unsigned long lastLogTime[4] = {0};
void log_throttled(uint8_t logId, uint8_t level, const char* msg) {
unsigned long now = millis();
unsigned long throttleMs = (level == 0) ? 5000UL :
(level == 1) ? 3000UL :
(level == 2) ? 2000UL : 1000UL;
if (now - lastLogTimeGlobal < 400UL) return;
if (now - lastLogTime[level] < throttleMs) return;
lastLogTime[level] = now;
lastLogTimeGlobal = now;
Serial.print(F("[LOG"));
Serial.print(logId);
Serial.print(F("] "));
if (level == 0) Serial.print(F("ERROR: "));
else if (level == 1) Serial.print(F("PERINGATAN: "));
else if (level == 2) Serial.print(F("INFO: "));
else Serial.print(F("DEBUG: "));
Serial.println(msg);
}
void log_throttled_f(uint8_t logId, uint8_t level, const char* fmt, ...) {
unsigned long now = millis();
unsigned long throttleMs = (level == 0) ? 5000UL :
(level == 1) ? 3000UL :
(level == 2) ? 2000UL : 1000UL;
if (now - lastLogTimeGlobal < 400UL) return;
if (now - lastLogTime[level] < throttleMs) return;
lastLogTime[level] = now;
lastLogTimeGlobal = now;
char buf[96];
va_list args;
va_start(args, fmt);
vsnprintf(buf, sizeof(buf), fmt, args);
va_end(args);
Serial.print(F("[LOG"));
Serial.print(logId);
Serial.print(F("] "));
if (level == 0) Serial.print(F("ERROR: "));
else if (level == 1) Serial.print(F("PERINGATAN: "));
else if (level == 2) Serial.print(F("INFO: "));
else Serial.print(F("DEBUG: "));
Serial.println(buf);
}
#endif
// ================= TUNING =================
const unsigned long STABILIZE_TIME = 250;
const unsigned long DEADTIME = 2200;
const unsigned long MAX_RUN_TIME = 16000;
const unsigned long SOFT_STOP_DELAY = 150;
const unsigned long RELAY_INTERLOCK_MS = 5;
const unsigned long RESET_HOLD_TIME = 4000;
const unsigned long CALIB_HOLD_TIME = 7000;
const unsigned long BOOT_GRACE_PERIOD = 5000;
const unsigned long SAFE_MODE_TIMEOUT = 30UL * 60UL * 1000UL;
const unsigned long LOCKOUT_RECOVERY_TIME = 24UL * 60UL * 60UL * 1000UL;
const unsigned long EEPROM_SAVE_COOLDOWN = 3600000UL;
const unsigned long EEPROM_DELTA_THRESHOLD = 500;
const unsigned long HOMING_TIMEOUT = 20000UL;
const unsigned long HOMING_ATTEMPT_GAP = 8000UL;
const uint8_t HOMING_FAIL_LOCKOUT = 8;
const uint8_t MAX_ERROR_COUNT_BEFORE_LOCK = 3;
const uint8_t BUZZER_QUEUE_SIZE = 10;
// ================= PIN =================
#define RTC_INTERRUPT_PIN 2
const int PIN_RELAY_PWR = 3;
const int PIN_RELAY_DIR = 4;
const int PIN_LIMIT_CLOSED = 12;
const int PIN_LIMIT_OPEN = 13;
const int DIP_RAIN = 5;
const int DIP_MODE = 6;
const int DIP_LDR = 7;
const int BTN_OPEN = 10;
const int BTN_CLOSE = 11;
const int PIN_BUZZER = 9;
const int PIN_LED = 8;
const int PIN_VT_RF = A3;
const int RF_OPEN = A0;
const int RF_CLOSE = A1;
const int RF_STOP = A2;
const int PIN_RAIN = A6;
const int PIN_LDR = A7;
// EEPROM addresses
const int EEPROM_LOG_START = 0;
const int EEPROM_LOG_INDEX_ADDR = 100;
const int EEPROM_MAX_ENTRIES = 20;
const int EEPROM_LDR_DARK_ADDR = 120;
const int EEPROM_LDR_BRIGHT_ADDR = 122;
const int EEPROM_RAIN_WET_ADDR = 130;
const int EEPROM_RAIN_DRY_ADDR = 132;
const int EEPROM_AVG_CW_ADDR = 140;
const int EEPROM_AVG_CCW_ADDR = 144;
// ================= STATE & GLOBAL =================
MotorState currentState = IDLE;
unsigned long lastStateChange = 0;
unsigned long stopStartTime = 0;
bool isClosedPosition = false;
bool positionKnown = true;
bool bootGraceDone = false;
unsigned long bootStartTime = 0;
bool lastBright = false;
bool lastRaining = false;
bool errorCondition = false;
unsigned long errorStartTime = 0;
bool rtcFailed = false;
bool inSafeMode = false;
bool lockoutMode = false;
unsigned long safeModeStart = 0;
uint8_t consecutiveErrorCount = 0;
int eepromLogIndex = 0;
unsigned long avgRunCW = 8000;
unsigned long avgRunCCW = 8000;
int ldrDarkThreshold = 320;
int ldrBrightThreshold = 880;
bool inLdrCalibration = false;
unsigned long ldrCalibStart = 0;
bool ldrCalibDarkSet = false;
bool ldrCalibBrightSet = false;
int rainWetThreshold = 600;
int rainDryThreshold = 850;
const int RAIN_HYSTERESIS = 60;
bool inRainCalibration = false;
unsigned long rainCalibStart = 0;
bool rainCalibWetSet = false;
bool rainCalibDrySet = false;
unsigned long lastEepromSave = 0;
unsigned long lastSavedCW = 8000;
unsigned long lastSavedCCW = 8000;
unsigned long homingStart = 0;
unsigned long lastHomingAttempt = 0;
uint8_t homingFailCount = 0;
struct BuzzerEvent { uint16_t freq; uint16_t dur; };
BuzzerEvent buzzerQueue[BUZZER_QUEUE_SIZE];
uint8_t queueHead = 0, queueTail = 0;
unsigned long buzzerEndTime = 0;
unsigned long nextBeepGap = 0;
bool resetComboPressed = false;
unsigned long resetComboStart = 0;
RTC_DS3231 rtc;
volatile bool alarmTriggered = false;
// ================= EEPROM FUNCTIONS =================
void initEepromLog() {
eepromLogIndex = EEPROM.read(EEPROM_LOG_INDEX_ADDR);
if (eepromLogIndex >= EEPROM_MAX_ENTRIES || eepromLogIndex < 0) {
eepromLogIndex = 0;
EEPROM.update(EEPROM_LOG_INDEX_ADDR, 0);
}
}
void loadLdrCalibration() {
ldrDarkThreshold = (EEPROM.read(EEPROM_LDR_DARK_ADDR) << 8) | EEPROM.read(EEPROM_LDR_DARK_ADDR + 1);
ldrBrightThreshold = (EEPROM.read(EEPROM_LDR_BRIGHT_ADDR) << 8) | EEPROM.read(EEPROM_LDR_BRIGHT_ADDR + 1);
if (ldrDarkThreshold < 100 || ldrDarkThreshold > 600 || ldrBrightThreshold < 700 || ldrBrightThreshold > 950 ||
(ldrBrightThreshold - ldrDarkThreshold) < 150) {
ldrDarkThreshold = 320; ldrBrightThreshold = 880;
}
}
void saveLdrCalibration() {
if ((ldrBrightThreshold - ldrDarkThreshold) < 150) {
queueBuzzer(400, 800);
loadLdrCalibration();
return;
}
EEPROM.update(EEPROM_LDR_DARK_ADDR, ldrDarkThreshold >> 8);
EEPROM.update(EEPROM_LDR_DARK_ADDR + 1, ldrDarkThreshold & 0xFF);
EEPROM.update(EEPROM_LDR_BRIGHT_ADDR, ldrBrightThreshold >> 8);
EEPROM.update(EEPROM_LDR_BRIGHT_ADDR + 1, ldrBrightThreshold & 0xFF);
queueBuzzer(1800, 300);
}
void loadRainCalibration() {
rainWetThreshold = (EEPROM.read(EEPROM_RAIN_WET_ADDR) << 8) | EEPROM.read(EEPROM_RAIN_WET_ADDR + 1);
rainDryThreshold = (EEPROM.read(EEPROM_RAIN_DRY_ADDR) << 8) | EEPROM.read(EEPROM_RAIN_DRY_ADDR + 1);
if (rainWetThreshold < 100 || rainWetThreshold > 700 || rainDryThreshold < 700 || rainDryThreshold > 1020 ||
(rainDryThreshold - rainWetThreshold) < 250) {
rainWetThreshold = 600; rainDryThreshold = 850;
}
}
void saveRainCalibration() {
if ((rainDryThreshold - rainWetThreshold) < 250) {
queueBuzzer(400, 800);
loadRainCalibration();
return;
}
EEPROM.update(EEPROM_RAIN_WET_ADDR, rainWetThreshold >> 8);
EEPROM.update(EEPROM_RAIN_WET_ADDR + 1, rainWetThreshold & 0xFF);
EEPROM.update(EEPROM_RAIN_DRY_ADDR, rainDryThreshold >> 8);
EEPROM.update(EEPROM_RAIN_DRY_ADDR + 1, rainDryThreshold & 0xFF);
queueBuzzer(1600, 250);
}
void loadAdaptiveAvg() {
EEPROM.get(EEPROM_AVG_CW_ADDR, avgRunCW);
EEPROM.get(EEPROM_AVG_CCW_ADDR, avgRunCCW);
if (avgRunCW < 2000 || avgRunCW > 20000) avgRunCW = 8000;
if (avgRunCCW < 2000 || avgRunCCW > 20000) avgRunCCW = 8000;
}
void saveAdaptiveAvgProtected() {
unsigned long now = millis();
if (now - lastEepromSave < EEPROM_SAVE_COOLDOWN) return;
bool needSave = false;
if (abs((long)avgRunCW - (long)lastSavedCW) > EEPROM_DELTA_THRESHOLD &&
avgRunCW >= 2000 && avgRunCW <= 20000) {
EEPROM.put(EEPROM_AVG_CW_ADDR, avgRunCW);
lastSavedCW = avgRunCW;
needSave = true;
}
if (abs((long)avgRunCCW - (long)lastSavedCCW) > EEPROM_DELTA_THRESHOLD &&
avgRunCCW >= 2000 && avgRunCCW <= 20000) {
EEPROM.put(EEPROM_AVG_CCW_ADDR, avgRunCCW);
lastSavedCCW = avgRunCCW;
needSave = true;
}
if (needSave) lastEepromSave = now;
}
// ================= BUZZER =================
void queueBuzzer(uint16_t freq, uint16_t dur) {
uint8_t next = (queueTail + 1) % BUZZER_QUEUE_SIZE;
if (next == queueHead) return;
buzzerQueue[queueTail] = {freq, dur};
queueTail = next;
}
void updateBuzzer() {
unsigned long now = millis();
if (buzzerEndTime > 0) {
if (now >= buzzerEndTime) {
noTone(PIN_BUZZER);
buzzerEndTime = 0;
nextBeepGap = now + 50;
}
} else if (queueHead != queueTail && now >= nextBeepGap) {
BuzzerEvent ev = buzzerQueue[queueHead];
tone(PIN_BUZZER, ev.freq, ev.dur);
buzzerEndTime = now + ev.dur;
queueHead = (queueHead + 1) % BUZZER_QUEUE_SIZE;
}
}
// ================= INPUT – HARDENED VERSION =================
bool readButton(int pin) {
static unsigned long debounceOpen = 0;
static unsigned long debounceClose = 0;
unsigned long* debouncePtr;
if (pin == BTN_OPEN) debouncePtr = &debounceOpen;
else if (pin == BTN_CLOSE) debouncePtr = &debounceClose;
else return false;
unsigned long now = millis();
if (now - *debouncePtr < 60) return false;
if (digitalRead(pin) == LOW) {
*debouncePtr = now;
return true;
}
return false;
}
bool readRF(int pin) {
static unsigned long tHigh[3] = {0}, lastValid[3] = {0};
static bool lastState[3] = {false};
int idx;
if (pin == RF_OPEN) idx = 0;
else if (pin == RF_CLOSE) idx = 1;
else if (pin == RF_STOP) idx = 2;
else return false;
bool nowVal = digitalRead(pin);
if (nowVal && !lastState[idx]) tHigh[idx] = millis();
bool valid = nowVal && (millis() - tHigh[idx] > 45);
if (valid) {
if (millis() - lastValid[idx] < 100) valid = false;
else lastValid[idx] = millis();
}
lastState[idx] = nowVal;
return valid;
}
bool readRFValidated(int pin) {
if (digitalRead(PIN_VT_RF) != HIGH) return false;
static unsigned long vtHighTime[3] = {0};
int idx;
if (pin == RF_OPEN) idx = 0;
else if (pin == RF_CLOSE) idx = 1;
else if (pin == RF_STOP) idx = 2;
else return false;
if (digitalRead(PIN_VT_RF) == HIGH) {
if (vtHighTime[idx] == 0) vtHighTime[idx] = millis();
if (millis() - vtHighTime[idx] < 50) return false;
} else {
vtHighTime[idx] = 0;
}
return readRF(pin);
}
bool isOpenPressed() { return readButton(BTN_OPEN) || readRFValidated(RF_OPEN); }
bool isClosePressed() { return readButton(BTN_CLOSE) || readRFValidated(RF_CLOSE); }
bool isStopPressed() { return readRFValidated(RF_STOP); }
// ================= SENSOR =================
bool isLimitClosedActive() { return digitalRead(PIN_LIMIT_CLOSED) == LOW; }
bool isLimitOpenActive() { return digitalRead(PIN_LIMIT_OPEN) == LOW; }
bool isAnyLimitActive() { return isLimitClosedActive() || isLimitOpenActive(); }
bool isRaining() {
if (digitalRead(DIP_RAIN) == HIGH) return false;
int val = analogRead(PIN_RAIN);
if (lastRaining && val > rainWetThreshold + RAIN_HYSTERESIS) lastRaining = false;
else if (!lastRaining && val < rainWetThreshold - RAIN_HYSTERESIS) lastRaining = true;
return lastRaining;
}
bool isBright() {
if (digitalRead(DIP_LDR) == HIGH) return true;
int val = analogRead(PIN_LDR);
if (lastBright && val < ldrDarkThreshold) lastBright = false;
else if (!lastBright && val > ldrBrightThreshold) lastBright = true;
return lastBright;
}
// ================= RELAY =================
void safeSetDirection(bool cw) {
digitalWrite(PIN_RELAY_PWR, LOW);
delayMicroseconds(RELAY_INTERLOCK_MS * 1000);
digitalWrite(PIN_RELAY_DIR, cw ? LOW : HIGH);
delayMicroseconds(500);
}
// ================= GLOBAL STOP =================
void checkGlobalStop() {
if (isStopPressed() &&
(currentState == START_CW || currentState == START_CCW ||
currentState == RUNNING_CW || currentState == RUNNING_CCW)) {
digitalWrite(PIN_RELAY_PWR, LOW);
queueHead = queueTail = 0;
queueBuzzer(3200, 250);
if (currentState != STOPPING) {
currentState = STOPPING;
lastStateChange = millis();
stopStartTime = millis();
}
}
}
// ================= CHANGE STATE =================
void changeState(MotorState s) {
if (currentState == s) return;
currentState = s;
lastStateChange = millis();
if (s == STOPPING) {
stopStartTime = millis();
}
if (s == ERROR) {
consecutiveErrorCount++;
if (consecutiveErrorCount >= MAX_ERROR_COUNT_BEFORE_LOCK) {
lockoutMode = true;
inSafeMode = true;
safeModeStart = millis();
LOG_ERROR(57, "Error berulang - masuk lockout permanen");
queueBuzzer(400, 1000);
}
} else {
consecutiveErrorCount = 0;
}
if (s == IDLE || s == COOLDOWN || s == ERROR) queueBuzzer(1200, 80);
}
// ================= AUTO HOMING =================
void tryAutoHome() {
unsigned long now = millis();
if (currentState == ERROR || lockoutMode) return;
if (!positionKnown && currentState == IDLE && !inSafeMode && !inLdrCalibration && !inRainCalibration) {
if (homingFailCount >= HOMING_FAIL_LOCKOUT) {
lockoutMode = true;
inSafeMode = true;
safeModeStart = now;
LOG_ERROR(49, "Homing gagal berulang - masuk lockout");
queueBuzzer(300, 1200); queueBuzzer(400, 1200);
return;
}
if (now - lastHomingAttempt < HOMING_ATTEMPT_GAP) return;
if (isLimitClosedActive()) {
positionKnown = true;
isClosedPosition = true;
homingStart = 0;
lastHomingAttempt = now;
homingFailCount = 0;
LOG_INFO(50, "Homing berhasil - posisi tertutup");
queueBuzzer(1200, 100); queueBuzzer(1800, 100);
return;
}
if (homingStart == 0) {
homingStart = now;
lastHomingAttempt = now;
changeState(START_CCW);
LOG_DEBUG(51, "Mulai homing otomatis CCW");
queueBuzzer(1000, 200); queueBuzzer(600, 400);
}
if (now - homingStart > HOMING_TIMEOUT) {
homingFailCount++;
changeState(ERROR);
LOG_ERROR(48, "Homing timeout - gagal");
queueBuzzer(400, 800);
homingStart = 0;
lastHomingAttempt = now + 10000UL;
}
} else {
homingStart = 0;
}
}
// ================= LIMIT UPDATE =================
void updateLimit() {
if (!bootGraceDone || inSafeMode) return;
bool limClosed = isLimitClosedActive();
bool limOpen = isLimitOpenActive();
if (currentState == RUNNING_CW && limClosed) {
changeState(ERROR);
LOG_ERROR(45, "Motor CW tapi limit tertutup aktif");
queueBuzzer(1800, 400);
return;
}
if (currentState == RUNNING_CCW && limOpen) {
changeState(ERROR);
LOG_ERROR(46, "Motor CCW tapi limit terbuka aktif");
queueBuzzer(1800, 400);
return;
}
if (!positionKnown && currentState == RUNNING_CCW && limClosed) {
positionKnown = true;
isClosedPosition = true;
homingStart = 0;
lastHomingAttempt = millis();
homingFailCount = 0;
LOG_INFO(50, "Homing berhasil via limit");
queueBuzzer(1800, 100); queueBuzzer(2200, 100);
changeState(STOPPING);
return;
}
if (currentState == RUNNING_CW && limOpen) {
isClosedPosition = false;
homingFailCount = 0;
changeState(STOPPING);
} else if (currentState == RUNNING_CCW && limClosed) {
isClosedPosition = true;
homingFailCount = 0;
changeState(STOPPING);
}
static unsigned long anomalyTimer = 0;
if (currentState == IDLE) {
if ((limClosed && !isClosedPosition) || (limOpen && isClosedPosition) || (limClosed && limOpen)) {
if (millis() - anomalyTimer > 300) {
changeState(ERROR);
errorCondition = true;
errorStartTime = millis();
LOG_ERROR(41, "Anomali limit switch");
queueBuzzer(1600, 300);
}
} else anomalyTimer = millis();
}
}
// ================= UPDATE MOTOR =================
void updateMotor() {
unsigned long now = millis();
if ((currentState == START_CW || currentState == START_CCW) && isAnyLimitActive()) {
changeState(STOPPING);
LOG_WARN(52, "Limit aktif saat START - proteksi wiring");
queueBuzzer(2800, 120);
return;
}
switch (currentState) {
case IDLE:
digitalWrite(PIN_RELAY_PWR, LOW);
break;
case START_CW:
digitalWrite(PIN_RELAY_PWR, LOW);
safeSetDirection(true);
if (now - lastStateChange >= STABILIZE_TIME) changeState(RUNNING_CW);
break;
case START_CCW:
digitalWrite(PIN_RELAY_PWR, LOW);
safeSetDirection(false);
if (now - lastStateChange >= STABILIZE_TIME) changeState(RUNNING_CCW);
break;
case RUNNING_CW:
case RUNNING_CCW:
digitalWrite(PIN_RELAY_PWR, HIGH);
if (now - lastStateChange >= MAX_RUN_TIME) changeState(STOPPING);
break;
case STOPPING:
digitalWrite(PIN_RELAY_PWR, LOW);
if (now - stopStartTime >= SOFT_STOP_DELAY) {
changeState(COOLDOWN);
}
break;
case COOLDOWN:
if (now - lastStateChange >= DEADTIME) changeState(IDLE);
break;
case ERROR:
digitalWrite(PIN_RELAY_PWR, LOW);
break;
}
unsigned long stallThresh = (currentState == RUNNING_CW ? avgRunCW : avgRunCCW) * 170UL / 100 + 4000;
if ((currentState == RUNNING_CW || currentState == RUNNING_CCW) &&
now - lastStateChange > stallThresh && !isAnyLimitActive()) {
changeState(ERROR);
errorCondition = true;
errorStartTime = now;
LOG_WARNF(40, "Stall terdeteksi setelah %lu ms", now - lastStateChange);
queueBuzzer(1800, 600);
}
}
// ================= COMBO BUTTON =================
void checkButtonCombos() {
unsigned long now = millis();
bool openP = isOpenPressed();
bool closeP = isClosePressed();
bool stopP = isStopPressed();
if (openP && closeP && !stopP) {
if (!resetComboPressed) { resetComboStart = now; resetComboPressed = true; }
if (now - resetComboStart >= RESET_HOLD_TIME) {
emergencyReset();
queueBuzzer(800, 150);
resetComboPressed = false;
}
} else resetComboPressed = false;
static bool ldrCombo = false; static unsigned long ldrStart = 0;
if (openP && stopP && !closeP) {
if (!ldrCombo) { ldrStart = now; ldrCombo = true; }
if (now - ldrStart >= CALIB_HOLD_TIME && !inLdrCalibration) {
inLdrCalibration = true;
ldrCalibDarkSet = ldrCalibBrightSet = false;
ldrCalibStart = now;
currentState = IDLE; digitalWrite(PIN_RELAY_PWR, LOW);
queueBuzzer(1400, 300); queueBuzzer(2200, 400);
ldrCombo = false;
}
} else ldrCombo = false;
static bool rainCombo = false; static unsigned long rainStart = 0;
if (closeP && stopP && !openP) {
if (!rainCombo) { rainStart = now; rainCombo = true; }
if (now - rainStart >= CALIB_HOLD_TIME && !inRainCalibration) {
inRainCalibration = true;
rainCalibWetSet = rainCalibDrySet = false;
rainCalibStart = now;
currentState = IDLE; digitalWrite(PIN_RELAY_PWR, LOW);
queueBuzzer(1200, 300); queueBuzzer(1800, 400);
rainCombo = false;
}
} else rainCombo = false;
if (inLdrCalibration) {
if (openP && !closeP && !stopP && !ldrCalibBrightSet) {
ldrBrightThreshold = analogRead(PIN_LDR);
ldrCalibBrightSet = true;
queueBuzzer(2500, 150);
}
if (closeP && !openP && !stopP && !ldrCalibDarkSet) {
ldrDarkThreshold = analogRead(PIN_LDR);
ldrCalibDarkSet = true;
queueBuzzer(900, 150);
}
if (ldrCalibDarkSet && ldrCalibBrightSet) {
saveLdrCalibration();
inLdrCalibration = false;
}
if (now - ldrCalibStart > 40000UL) {
inLdrCalibration = false;
queueBuzzer(300, 1200);
}
}
if (inRainCalibration) {
if (closeP && !openP && !stopP && !rainCalibWetSet) {
rainWetThreshold = analogRead(PIN_RAIN);
rainCalibWetSet = true;
queueBuzzer(900, 200);
}
if (openP && !closeP && !stopP && !rainCalibDrySet) {
rainDryThreshold = analogRead(PIN_RAIN);
rainCalibDrySet = true;
queueBuzzer(2500, 200);
}
if (rainCalibWetSet && rainCalibDrySet) {
saveRainCalibration();
inRainCalibration = false;
}
if (now - rainCalibStart > 40000UL) {
inRainCalibration = false;
queueBuzzer(300, 1200);
}
}
}
// ================= CONTROL =================
void handleControl() {
bool jog = digitalRead(DIP_MODE) == HIGH;
if (jog) {
if (currentState == IDLE) {
if (isOpenPressed() && !isClosePressed()) changeState(START_CW);
if (isClosePressed() && !isOpenPressed()) changeState(START_CCW);
} else if (isOpenPressed() || isClosePressed()) changeState(STOPPING);
} else if (currentState == IDLE && positionKnown && !inSafeMode && !lockoutMode) {
if (isOpenPressed() && isClosedPosition && !isClosePressed()) changeState(START_CW);
if (isClosePressed() && !isClosedPosition && !isOpenPressed()) changeState(START_CCW);
}
}
// ================= AUTO LOGIC =================
void autoLogic() {
if (inSafeMode || lockoutMode || !positionKnown || digitalRead(DIP_MODE) == HIGH || currentState != IDLE) return;
if (isRaining() && !isClosedPosition) {
changeState(START_CCW);
return;
}
bool sun = isBright();
if (!sun && !isClosedPosition) changeState(START_CCW);
else if (sun && isClosedPosition) changeState(START_CW);
}
// ================= RTC ALARM HANDLER =================
void handleRTCAlarm() {
if (rtcFailed || !alarmTriggered) return;
alarmTriggered = false;
DateTime nowDt = rtc.now();
if (rtc.alarmFired(1)) {
rtc.clearAlarm(1);
rtc.setAlarm1(nowDt + TimeSpan(0, 24, 0, 0), DS3231_A1_Hour);
if (isClosedPosition && currentState == IDLE) {
changeState(START_CW);
}
}
if (rtc.alarmFired(2)) {
rtc.clearAlarm(2);
rtc.setAlarm2(nowDt + TimeSpan(1, 0, 0, 0), DS3231_A2_Hour);
if (!isClosedPosition && currentState == IDLE) {
changeState(START_CCW);
}
}
}
// ================= EMERGENCY RESET =================
void emergencyReset() {
digitalWrite(PIN_RELAY_PWR, LOW);
digitalWrite(PIN_RELAY_DIR, LOW);
currentState = IDLE;
errorCondition = false;
consecutiveErrorCount = 0;
lockoutMode = false;
inSafeMode = false;
homingFailCount = 0;
stopStartTime = 0;
LOG_INFO(60, "Reset darurat berhasil");
queueBuzzer(800, 150); queueBuzzer(1200, 150);
}
// ================= LOW POWER =================
void enterLowPowerSleep() {
digitalWrite(PIN_RELAY_PWR, LOW);
digitalWrite(PIN_RELAY_DIR, LOW);
digitalWrite(PIN_LED, LOW);
digitalWrite(PIN_BUZZER, LOW);
ADCSRA &= ~(1 << ADEN);
wdt_disable();
LowPower.powerDown(SLEEP_8S, ADC_OFF, BOD_OFF);
wdt_enable(WDTO_8S);
ADCSRA |= (1 << ADEN);
}
// ================= SAFE MODE & LED =================
void updateSafeMode() {
unsigned long now = millis();
if (inSafeMode && !lockoutMode && now - safeModeStart > SAFE_MODE_TIMEOUT) {
inSafeMode = false;
LOG_INFO(56, "Safe mode selesai");
queueBuzzer(2400, 200); queueBuzzer(1800, 200);
}
if (lockoutMode && now - safeModeStart > LOCKOUT_RECOVERY_TIME) {
lockoutMode = false;
inSafeMode = false;
homingFailCount = 0;
LOG_INFO(58, "Lockout auto recovery setelah 24 jam");
queueBuzzer(1600, 150); queueBuzzer(2000, 150);
}
}
void updateLED() {
if (lockoutMode) {
digitalWrite(PIN_LED, (millis() % 100) < 50);
} else if (inSafeMode) {
digitalWrite(PIN_LED, (millis() % 150) < 75);
} else if (!positionKnown && homingStart > 0) {
digitalWrite(PIN_LED, (millis() % 200) < 100);
} else if (errorCondition) {
digitalWrite(PIN_LED, (millis() % 250) < 125);
} else if (currentState == RUNNING_CW || currentState == RUNNING_CCW) {
digitalWrite(PIN_LED, (millis() % 800) < 400);
} else {
digitalWrite(PIN_LED, (positionKnown && isClosedPosition) ? LOW : HIGH);
}
}
// ================= ISR =================
void onRTCAlarm() {
alarmTriggered = true;
}
// ================= SETUP =================
void setup() {
digitalWrite(PIN_RELAY_PWR, LOW);
digitalWrite(PIN_RELAY_DIR, LOW);
delay(5);
#if DEBUG_LEVEL > 0
Serial.begin(9600);
Serial.println(F("=== KANOPI V5.28 - MODE DEBUG AKTIF (Level "));
Serial.print(DEBUG_LEVEL);
Serial.println(F(") ==="));
#else
// Serial tidak diinisialisasi sama sekali di produksi
#endif
wdt_enable(WDTO_8S);
initEepromLog();
loadLdrCalibration();
loadRainCalibration();
loadAdaptiveAvg();
homingFailCount = 0;
stopStartTime = 0;
queueHead = queueTail = 0;
byte mcusr = MCUSR;
if (mcusr & (1 << WDRF)) {
inSafeMode = true;
safeModeStart = millis();
LOG_ERROR(55, "Watchdog reset terdeteksi");
}
MCUSR = 0;
rtcFailed = !rtc.begin();
if (!rtcFailed) {
if (rtc.lostPower()) rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));
rtc.clearAlarm(1);
rtc.clearAlarm(2);
rtc.setAlarm1(DateTime(2026,1,1,6,0,0), DS3231_A1_Hour);
rtc.setAlarm2(DateTime(2026,1,1,18,0,0), DS3231_A2_Hour);
rtc.writeSqwPinMode(DS3231_OFF);
pinMode(RTC_INTERRUPT_PIN, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(RTC_INTERRUPT_PIN), onRTCAlarm, FALLING);
}
pinMode(PIN_RELAY_PWR, OUTPUT);
pinMode(PIN_RELAY_DIR, OUTPUT);
pinMode(PIN_BUZZER, OUTPUT);
pinMode(PIN_LED, OUTPUT);
pinMode(PIN_LIMIT_CLOSED, INPUT_PULLUP);
pinMode(PIN_LIMIT_OPEN, INPUT_PULLUP);
pinMode(BTN_OPEN, INPUT_PULLUP);
pinMode(BTN_CLOSE, INPUT_PULLUP);
bool limC = isLimitClosedActive();
bool limO = isLimitOpenActive();
if (limC || limO) {
positionKnown = true;
isClosedPosition = limC;
homingFailCount = 0;
LOG_DEBUGF(47, "Boot: Limit C=%d O=%d", limC, limO);
} else {
positionKnown = false;
LOG_ERROR(47, "Posisi tidak diketahui saat boot");
}
bootStartTime = millis();
queueBuzzer(2200, 150);
LOG_INFO(0, "Firmware V5.28 siap - Produksi Indonesia (BUGFIX applied)");
}
// ================= LOOP =================
void loop() {
wdt_reset();
handleRTCAlarm();
updateBuzzer();
updateSafeMode();
if (millis() - bootStartTime > BOOT_GRACE_PERIOD) bootGraceDone = true;
checkGlobalStop();
checkButtonCombos();
handleControl();
tryAutoHome();
autoLogic();
updateLimit();
updateMotor();
updateLED();
bool inputActive = isOpenPressed() || isClosePressed() || isStopPressed();
if (currentState == IDLE && !isRaining() && !alarmTriggered && !inputActive && !errorCondition &&
!inLdrCalibration && !inRainCalibration && !inSafeMode && !lockoutMode) {
enterLowPowerSleep();
}
if (currentState == COOLDOWN) {
saveAdaptiveAvgProtected();
}
}