/*
* Firmware Jemuran Otomatis - Versi Relay
* Versi: 1.4.2
* Build Date: __DATE__ __TIME__
*
* Fitur utama:
* - Rain sensor stabil dengan konfirmasi bertingkat (anti noise)
* - Jemuran TIDAK BOLEH dibuka selama hujan aktif atau masa tunggu kering 30 menit
* - OPEN hanya diizinkan saat sensor benar-benar kering lama
* - RTC threshold tahun < 2025 (cocok untuk 2026)
* - Factory mode dengan jog timeout & blokir buka saat hujan
* - Overcurrent
* - EEPROM CRC, logging fault rate-limited
*/
#include <Arduino.h>
#include <IRremote.hpp>
#include <EEPROM.h>
#include <avr/wdt.h>
#define USE_RTC
#ifdef USE_RTC
#include <Wire.h>
#include "RTClib.h"
RTC_DS3231 rtc;
#endif
#define DEBUG_MODE
#ifdef DEBUG_MODE
#define DEBUG_PRINT(...) Serial.print(__VA_ARGS__)
#define DEBUG_PRINTLN(...) Serial.println(__VA_ARGS__)
#else
#define DEBUG_PRINT(...)
#define DEBUG_PRINTLN(...)
#endif
#define FIRMWARE_VERSION "1.4.2"
/* ========================= PIN CONFIG ========================= */
#define PIN_IR 2
#define PIN_LS_OPEN 5
#define PIN_LS_CLOSE 6
#define PIN_RAIN 11
#define PIN_LED 13
#define PIN_BUZZER 12
#define PIN_JOG_OPEN A1
#define PIN_JOG_CLOSE A2
#define PIN_EXIT_F_MODE 4
#define PIN_RELAY_OPEN 9
#define PIN_RELAY_CLOSE 10
#define PIN_CURRENT A0
#define PIN_FACTORY A3
/* ========================= IR COMMAND ========================= */
#define CMD_IR_OPEN 0x90
#define CMD_IR_CLOSE 0xE0
#define CMD_IR_STOP 0x68
#define CMD_IR_RESET 0xA2
/* ========================= TIMING & THRESHOLD ========================= */
constexpr uint16_t IR_ACCEPT_TIMEOUT_MS = 800;
constexpr uint32_t RAIN_DRY_DELAY_MS = 180000UL; // 3 menit
constexpr uint16_t MOTOR_DEAD_TIME = 350;
constexpr uint16_t JOG_TIMEOUT_MS = 6000UL;
constexpr uint8_t RAIN_CONFIRM_COUNTS = 4;
constexpr uint8_t DRY_CONFIRM_COUNTS = 5;
constexpr uint16_t RAIN_SAMPLE_INTERVAL = 250;
/* ========================= EEPROM LAYOUT ========================= */
#define EEPROM_PARAM_ADDR 0
#define EEPROM_STATE_ADDR 64
#define EEPROM_LOG_ADDR 80
#define FAULT_ENTRIES 50
struct ParamBlock {
uint16_t signature; // magic number untuk validasi
uint16_t currentLimit; // batas arus motor
uint16_t moveTimeout; // timeout pergerakan motor (ms)
uint16_t rainConfirm; // waktu konfirmasi hujan (ms)
uint8_t openHour; // jam buka otomatis
uint8_t closeHour; // jam tutup otomatis
uint8_t crc; // checksum
uint8_t flags; // bit flag konfigurasi
int currentOffset; // offset sensor arus
};
struct FaultEntry {
uint8_t code;
uint8_t h, m, d, mo, y;
uint8_t crc;
};
ParamBlock params;
const ParamBlock defaultParams PROGMEM = {
0xBEEF, // signature
600, // currentLimit (mA)
15000, // moveTimeout (ms)
3000, // rainConfirm (ms)
7, // openHour
17, // closeHour
0, // crc (akan dihitung)
0, // flags default
540 // currentOffset
};
/* ========================= STATE MACHINE ========================= */
enum State { CLOSED, OPENING, OPEN, CLOSING, STOPPED, BRAKING, FAULT, FAULT_LOCKED };
State currentState = STOPPED;
State nextStateAfterBrake = STOPPED;
/* ========================= GLOBAL VARIABLES ========================= */
unsigned long brakeStartTime = 0;
unsigned long moveStart = 0;
unsigned long rainTimer = 0;
unsigned long rainStoppedTime = 0;
unsigned long lastDirectionChange = 0;
unsigned long lastCmdAcceptTime = 0;
unsigned long lastIR = 0;
unsigned long lastMotorStartTime = 0;
unsigned long lastJogOpenChange = 0;
unsigned long lastJogCloseChange = 0;
bool lastJogOpenRaw = HIGH;
bool lastJogCloseRaw = HIGH;
bool stableOpen = false;
bool stableClose = false;
unsigned long jogStartTime = 0;
bool jogging = false;
bool jogDirectionOpen = false;
// Variabel global untuk kalibrasi non-blocking
bool calibrating = false;
unsigned long calibStart = 0;
unsigned long lastSample = 0;
long calibSum = 0;
uint8_t calibCount = 0;
int filteredCurrent = 0;
unsigned long lastRainStableTime = 0;
bool lastStableRainState = false;
uint8_t rainConfirmCounter = 0;
uint8_t dryConfirmCounter = 0;
uint8_t lastAcceptedCmd = 0;
const unsigned long DEBOUNCE_TIME_MS = 30UL;
const unsigned long HEARTBEAT_INTERVAL = 500UL;
const unsigned long BEEP_DURATION = 150UL;
#define ACTIVE_BRAKE_DURATION 150
static unsigned long factoryBrakeStart = 0;
static bool factoryBraking = false;
static unsigned long buzzerSilenceTime = 0;
bool emergencyBuzzerActive = false;
unsigned long nextSosCycleTime = 0;
uint8_t sosStep = 0;
uint8_t sosRepeatCount = 0;
unsigned long sosNextTime = 0;
const uint16_t EMERGENCY_SOS_PATTERN[] = {100,100,100,100,100,300,400,100,400,100,400,300,100,100,100,100,100,1000};
constexpr uint8_t EMERGENCY_SOS_STEPS = sizeof(EMERGENCY_SOS_PATTERN)/sizeof(uint16_t);
struct LogThrottle {
unsigned long lastLogTime = 0;
uint8_t burstCounter = 0;
unsigned long lastSerialPrint = 0;
} logThrottle;
/* ========================= CRC8 ========================= */
uint8_t crc8(const uint8_t *data, uint8_t len) {
uint8_t crc = 0;
while (len--) {
crc ^= *data++;
for (uint8_t i = 0; i < 8; i++)
crc = (crc & 0x80) ? (crc << 1) ^ 0x07 : crc << 1;
}
return crc;
}
/* ========================= EEPROM ========================= */
void saveParams() {
params.crc = crc8((uint8_t*)¶ms, sizeof(ParamBlock)-1);
EEPROM.put(EEPROM_PARAM_ADDR, params);
}
void loadParams() {
EEPROM.get(EEPROM_PARAM_ADDR, params);
if (params.signature != 0xBEEF || crc8((uint8_t*)¶ms, sizeof(ParamBlock)-1) != params.crc) {
memcpy_P(¶ms, &defaultParams, sizeof(ParamBlock));
saveParams();
}
}
/* ========================= HELPERS - BUZZER, HEARTBEAT, CLEAR LOG ========================= */
void triggerBuzzer(unsigned long duration) {
digitalWrite(PIN_BUZZER, LOW);
buzzerSilenceTime = millis() + duration;
}
void updateBuzzerManager() {
if (buzzerSilenceTime > 0 && millis() >= buzzerSilenceTime) {
digitalWrite(PIN_BUZZER, HIGH);
buzzerSilenceTime = 0;
}
}
void heartbeatIndicator(unsigned long now) {
static unsigned long lastHeartbeat = 0;
static bool ledState = false;
if (now - lastHeartbeat >= HEARTBEAT_INTERVAL) {
lastHeartbeat = now;
ledState = !ledState;
digitalWrite(PIN_LED, ledState);
if (ledState) triggerBuzzer(BEEP_DURATION);
}
}
void clearFaultLog() {
EEPROM.update(EEPROM_LOG_ADDR, 0);
#ifdef DEBUG_MODE
DEBUG_PRINTLN(F("Fault log direset"));
#endif
}
/* ========================= FAULT LOGGING ========================= */
void logFault_nonblocking(uint8_t code) {
FaultEntry f;
memset(&f, 0, sizeof(FaultEntry));
f.code = code;
#ifdef USE_RTC
DateTime nowRTC = rtc.now();
if (nowRTC.year() >= 2025 && nowRTC.year() < 2100) {
f.h = nowRTC.hour();
f.m = nowRTC.minute();
f.d = nowRTC.day();
f.mo = nowRTC.month();
f.y = (uint8_t)(nowRTC.year() - 2000);
}
#endif
f.crc = crc8((uint8_t*)&f, sizeof(FaultEntry) - 1);
uint8_t index = EEPROM.read(EEPROM_LOG_ADDR);
if (index >= FAULT_ENTRIES) index = 0;
uint16_t addr = EEPROM_LOG_ADDR + 1 + (index * sizeof(FaultEntry));
EEPROM.put(addr, f);
uint8_t nextIndex = (index + 1) % FAULT_ENTRIES;
EEPROM.update(EEPROM_LOG_ADDR, nextIndex);
unsigned long nowMs = millis();
if (nowMs - logThrottle.lastSerialPrint >= 1500UL) {
logThrottle.lastSerialPrint = nowMs;
#ifdef DEBUG_MODE
DEBUG_PRINT(F("[FAULT] Code: "));
DEBUG_PRINT(code);
const char* desc[] = {"", "Overcurrent", "Timeout", "LS Conflict", "Mechanical Jam"};
if (code < 5) DEBUG_PRINTLN(desc[code]);
else DEBUG_PRINTLN(" Unknown");
#endif
}
}
#define LOG_FAULT(code) do { \
unsigned long now = millis(); \
bool shouldLog = false; \
if (now - logThrottle.lastLogTime > 3000UL) { \
logThrottle.burstCounter = 0; \
} \
if (logThrottle.burstCounter < 3) { \
shouldLog = true; \
logThrottle.burstCounter++; \
} else if (now - logThrottle.lastLogTime >= 3000UL) { \
shouldLog = true; \
logThrottle.burstCounter = 1; \
} \
if (shouldLog) { \
logThrottle.lastLogTime = now; \
logFault_nonblocking(code); \
} \
} while(0)
/* ========================= RAIN HELPER ========================= */
bool isRainActiveOrRecent() {
unsigned long n = millis();
if (rainTimer != 0 && n - rainTimer >= 3000) return true;
if (rainStoppedTime != 0 && n - rainStoppedTime < RAIN_DRY_DELAY_MS) return true;
if (lastStableRainState) return true;
return false;
}
/* ========================= RAIN SENSOR ========================= */
void handleRainSensor() {
static unsigned long lastSample = 0;
unsigned long n = millis();
if (n - lastSample < RAIN_SAMPLE_INTERVAL) return;
lastSample = n;
bool wet = (digitalRead(PIN_RAIN) == LOW);
if (wet) {
dryConfirmCounter = 0;
if (!lastStableRainState) {
rainConfirmCounter++;
if (rainConfirmCounter >= RAIN_CONFIRM_COUNTS) {
// Jemuran harus ditutup jika belum dalam state CLOSE
if (currentState != CLOSING && currentState != CLOSED &&
currentState != FAULT && currentState != FAULT_LOCKED) {
requestState(CLOSING);
#ifdef DEBUG_MODE
DEBUG_PRINTLN(F("[EVENT] Rain detected, jemuran menutup"));
#endif
}
lastStableRainState = true;
}
}
} else {
rainConfirmCounter = 0;
if (lastStableRainState) {
dryConfirmCounter++;
if (dryConfirmCounter >= DRY_CONFIRM_COUNTS) {
lastStableRainState = false;
rainStoppedTime = n;
rainTimer = 0;
#ifdef DEBUG_MODE
DEBUG_PRINTLN(F("[EVENT] Rain stopped, mulai hitung delay kering"));
#endif
}
}
}
// Setelah delay kering, jemuran bisa buka otomatis
if (rainStoppedTime != 0 &&
n - rainStoppedTime >= RAIN_DRY_DELAY_MS &&
currentState == CLOSED &&
currentState != FAULT_LOCKED) {
requestState(OPENING);
rainStoppedTime = 0;
#ifdef DEBUG_MODE
DEBUG_PRINTLN(F("[EVENT] Delay kering selesai, jemuran membuka"));
#endif
}
// Reset timer jika jemuran sedang bergerak
if (currentState == OPENING || currentState == CLOSING) {
rainStoppedTime = 0;
}
}
/* ========================= MOTOR CONTROL ========================= */
// Helper
bool isLimitOpenReached() { return digitalRead(PIN_LS_OPEN) == LOW; }
bool isLimitCloseReached() { return digitalRead(PIN_LS_CLOSE) == LOW; }
void setMotorRelay(bool openRelay, bool closeRelay) {
// Pastikan tidak ada kondisi kedua relay aktif bersamaan
if (openRelay && closeRelay) {
// Fault: keduanya aktif, matikan semua
digitalWrite(PIN_RELAY_OPEN, LOW);
digitalWrite(PIN_RELAY_CLOSE, LOW);
return;
}
digitalWrite(PIN_RELAY_OPEN, openRelay ? HIGH : LOW);
digitalWrite(PIN_RELAY_CLOSE, closeRelay ? HIGH : LOW);
}
void motorStop() {
setMotorRelay(false, false);
}
void motorExecute(bool openDir) {
if (openDir) {
setMotorRelay(true, false); // Relay OPEN aktif
} else {
setMotorRelay(false, true); // Relay CLOSE aktif
}
}
/* ========================= REQUEST STATE ========================= */
void requestState(State newState) {
if (newState == currentState) return;
// Cegah buka saat hujan
if (newState == OPENING && isRainActiveOrRecent()) {
triggerBuzzer(400);
#ifdef DEBUG_MODE
DEBUG_PRINTLN(F("IR OPEN diblokir karena hujan"));
#endif
return;
}
// Fault jika kedua limit switch aktif bersamaan
if (isLimitOpenReached() && isLimitCloseReached()) {
currentState = FAULT_LOCKED;
LOG_FAULT(3); // LS Conflict
#ifdef DEBUG_MODE
DEBUG_PRINTLN(F("[FAULT] Limit switch conflict, masuk FAULT_LOCKED"));
#endif
return;
}
// Intervensi balik arah
if ((currentState == OPENING && newState == CLOSING) ||
(currentState == CLOSING && newState == OPENING)) {
motorStop();
brakeStartTime = millis();
nextStateAfterBrake = newState;
currentState = BRAKING;
moveStart = 0; // reset timeout agar nilai lama tidak menggantung
#ifdef DEBUG_MODE
DEBUG_PRINTLN(F("[EVENT] Intervensi balik arah, masuk BRAKING"));
#endif
return;
}
// Dead-time antar relay
if (millis() - lastDirectionChange < MOTOR_DEAD_TIME && newState != STOPPED) return;
lastDirectionChange = millis();
// Reset timer saat mulai gerakan baru
if (newState == OPENING || newState == CLOSING) {
moveStart = millis();
lastMotorStartTime = millis();
}
// Reset timeout saat motor berhenti normal
if (newState == STOPPED || newState == OPEN || newState == CLOSED) {
moveStart = 0;
#ifdef DEBUG_MODE
DEBUG_PRINTLN(F("[EVENT] Motor berhenti normal, timeout direset"));
#endif
}
currentState = newState;
EEPROM.update(EEPROM_STATE_ADDR, (uint8_t)currentState);
// Eksekusi relay sesuai state
if (currentState == OPENING) {
motorExecute(true);
} else if (currentState == CLOSING) {
motorExecute(false);
} else {
motorStop();
}
}
/* ========================= IR HANDLER ========================= */
void handleRemoteIR() {
if (!IrReceiver.decode()) {
if (millis() - lastIR > 1500) lastAcceptedCmd = 0;
return;
}
uint32_t now = millis();
uint8_t cmd = IrReceiver.decodedIRData.command;
// Repeat handling
if (IrReceiver.decodedIRData.flags & IRDATA_FLAGS_IS_REPEAT) {
if (lastAcceptedCmd == 0) { IrReceiver.resume(); return; }
cmd = lastAcceptedCmd;
} else {
lastAcceptedCmd = cmd;
}
// Abaikan command invalid
if (cmd == 0 || IrReceiver.decodedIRData.protocol == UNKNOWN) {
IrReceiver.resume(); return;
}
// STOP command → prioritas tertinggi
if (cmd == CMD_IR_STOP) {
if (now - lastCmdAcceptTime > 150) {
lastCmdAcceptTime = now;
requestState(STOPPED);
moveStart = 0; // reset timeout
#ifdef DEBUG_MODE
DEBUG_PRINTLN(F("IR STOP command diterima, timeout direset"));
#endif
triggerBuzzer(100);
}
IrReceiver.resume();
return;
}
// Abaikan jika sedang fault/braking atau baru start motor
if (currentState == FAULT || currentState == FAULT_LOCKED || currentState == BRAKING ||
now - lastMotorStartTime < 600) {
IrReceiver.resume(); return;
}
// Anti-spam IR
if (now - lastCmdAcceptTime < IR_ACCEPT_TIMEOUT_MS) {
IrReceiver.resume(); return;
}
lastCmdAcceptTime = now;
lastIR = now;
switch (cmd) {
case CMD_IR_OPEN:
if (isLimitOpenReached()) {
triggerBuzzer(200);
#ifdef DEBUG_MODE
DEBUG_PRINTLN(F("IR OPEN ditolak (limit tercapai)"));
#endif
} else if (currentState == CLOSING) {
requestState(OPENING); // balik arah → BRAKING → reset timeout di runEngine
#ifdef DEBUG_MODE
DEBUG_PRINTLN(F("IR OPEN command diterima (balik arah)"));
#endif
} else if (currentState == STOPPED || currentState == CLOSED) {
if (isRainActiveOrRecent()) {
triggerBuzzer(400);
#ifdef DEBUG_MODE
DEBUG_PRINTLN(F("IR OPEN diblokir karena hujan"));
#endif
} else {
requestState(OPENING);
#ifdef DEBUG_MODE
DEBUG_PRINTLN(F("IR OPEN command diterima"));
#endif
triggerBuzzer(50);
}
}
break;
case CMD_IR_CLOSE:
if (isLimitCloseReached()) {
triggerBuzzer(200);
#ifdef DEBUG_MODE
DEBUG_PRINTLN(F("IR CLOSE ditolak (limit tercapai)"));
#endif
} else if (currentState == OPENING) {
requestState(CLOSING); // balik arah → BRAKING → reset timeout di runEngine
#ifdef DEBUG_MODE
DEBUG_PRINTLN(F("IR CLOSE command diterima (balik arah)"));
#endif
} else if (currentState == STOPPED || currentState == OPEN) {
requestState(CLOSING);
#ifdef DEBUG_MODE
DEBUG_PRINTLN(F("IR CLOSE command diterima"));
#endif
triggerBuzzer(50);
}
break;
case CMD_IR_RESET:
clearFaultLog();
currentState = STOPPED;
moveStart = 0; // reset timeout
EEPROM.update(EEPROM_STATE_ADDR, (uint8_t)currentState);
#ifdef DEBUG_MODE
DEBUG_PRINTLN(F("IR RESET command dijalankan, timeout direset"));
#endif
triggerBuzzer(800);
break;
}
IrReceiver.resume();
}
/* ========================= ENGINE ========================= */
void runEngine() {
unsigned long now = millis();
switch (currentState) {
case BRAKING:
setMotorRelay(false, false);
if (now - brakeStartTime >= ACTIVE_BRAKE_DURATION) {
lastDirectionChange = now;
currentState = nextStateAfterBrake;
// Reset timer saat keluar dari BRAKING ke arah baru
if (currentState == OPENING || currentState == CLOSING) {
moveStart = millis();
lastMotorStartTime = millis();
#ifdef DEBUG_MODE
DEBUG_PRINTLN(F("[EVENT] Balik arah selesai, timeout direset"));
#endif
}
}
break;
case OPENING:
motorExecute(true);
if (isLimitOpenReached()) {
requestState(OPEN);
moveStart = 0; // reset timeout karena jemuran sudah berhenti normal
#ifdef DEBUG_MODE
DEBUG_PRINTLN(F("[EVENT] Jemuran mencapai limit OPEN, timeout direset"));
#endif
}
// Monitoring arus
{
int raw = abs(analogRead(PIN_CURRENT) - params.currentOffset);
filteredCurrent = (filteredCurrent * 7 + raw * 3) / 10;
if (moveStart != 0 && now - moveStart > 800) {
if (filteredCurrent > params.currentLimit + 40) {
logFault_nonblocking(1);
requestState(FAULT_LOCKED);
} else if (filteredCurrent < params.currentLimit - 50) {
filteredCurrent = 0;
}
}
// Timeout motor OPEN
if (moveStart != 0 && now - moveStart > params.moveTimeout) {
logFault_nonblocking(2);
requestState(FAULT_LOCKED);
#ifdef DEBUG_MODE
DEBUG_PRINTLN(F("[FAULT] Timeout OPEN, jemuran terkunci"));
#endif
}
}
break;
case CLOSING:
motorExecute(false);
if (isLimitCloseReached()) {
requestState(CLOSED);
moveStart = 0; // reset timeout karena jemuran sudah berhenti normal
#ifdef DEBUG_MODE
DEBUG_PRINTLN(F("[EVENT] Jemuran mencapai limit CLOSE, timeout direset"));
#endif
}
// Monitoring arus
{
int raw = abs(analogRead(PIN_CURRENT) - params.currentOffset);
filteredCurrent = (filteredCurrent * 7 + raw * 3) / 10;
if (moveStart != 0 && now - moveStart > 800) {
if (filteredCurrent > params.currentLimit + 40) {
logFault_nonblocking(1);
requestState(FAULT_LOCKED);
} else if (filteredCurrent < params.currentLimit - 50) {
filteredCurrent = 0;
}
}
// Timeout motor CLOSE
if (moveStart != 0 && now - moveStart > params.moveTimeout) {
logFault_nonblocking(2);
requestState(FAULT_LOCKED);
#ifdef DEBUG_MODE
DEBUG_PRINTLN(F("[FAULT] Timeout CLOSE, jemuran terkunci"));
#endif
}
}
break;
default:
motorStop();
break;
}
}
/* ========================= AUTO SCHEDULE ========================= */
void handleAutoSchedule() {
#ifdef USE_RTC
DateTime now = rtc.now();
if (now.year() < 2025) return;
static uint8_t lastDayOpened = 0;
static uint8_t lastDayClosed = 0;
if (now.hour() == params.openHour && lastDayOpened != now.day()) {
if (!isRainActiveOrRecent() && currentState == CLOSED) {
requestState(OPENING);
#ifdef DEBUG_MODE
DEBUG_PRINTLN(F("AutoSchedule OPEN dijalankan"));
#endif
lastDayOpened = now.day();
} else {
#ifdef DEBUG_MODE
DEBUG_PRINTLN(F("AutoSchedule OPEN dibatalkan (hujan)"));
#endif
}
}
if (now.hour() == params.closeHour && lastDayClosed != now.day()) {
if (currentState == OPEN) {
requestState(CLOSING);
#ifdef DEBUG_MODE
DEBUG_PRINTLN(F("AutoSchedule CLOSE dijalankan"));
#endif
lastDayClosed = now.day();
}
}
#endif
}
/* ========================= INDICATORS ========================= */
void updateIndicators() {
unsigned long now = millis();
if (currentState == FAULT || currentState == FAULT_LOCKED) {
if (!emergencyBuzzerActive && now >= nextSosCycleTime) {
emergencyBuzzerActive = true;
sosStep = 0; sosRepeatCount = 0; sosNextTime = now;
}
if (emergencyBuzzerActive) {
if (now >= sosNextTime) {
bool on = (sosStep % 2 == 0);
digitalWrite(PIN_BUZZER, on ? LOW : HIGH);
digitalWrite(PIN_LED, on ? HIGH : LOW);
sosNextTime = now + EMERGENCY_SOS_PATTERN[sosStep++];
if (sosStep >= EMERGENCY_SOS_STEPS) {
sosStep = 0;
if (++sosRepeatCount >= 3) {
emergencyBuzzerActive = false;
digitalWrite(PIN_BUZZER, HIGH);
nextSosCycleTime = now + 180000UL;
}
}
}
return;
}
} else {
emergencyBuzzerActive = false;
digitalWrite(PIN_BUZZER, HIGH);
}
uint16_t interval = (currentState == OPENING || currentState == CLOSING) ? 150 :
(currentState == BRAKING) ? 50 :
(currentState == FAULT || currentState == FAULT_LOCKED) ? 250 : 1000;
digitalWrite(PIN_LED, (now / interval) % 2);
}
/* ========================= FACTORY MODE ========================= */
bool getStableButton(bool cur, bool &last, unsigned long &chg, bool &stable, unsigned long now) {
if (cur != last) {
chg = now;
last = cur;
}
if (now - chg >= DEBOUNCE_TIME_MS) stable = cur;
return stable;
}
void startCalibrateCurrentOffset() {
calibrating = true;
calibStart = millis();
lastSample = millis();
calibSum = 0;
calibCount = 0;
}
void updateCalibrateCurrentOffset() {
if (!calibrating) return;
unsigned long now = millis();
// Sampling setiap 8 ms
if (now - lastSample >= 8) {
lastSample = now;
calibSum += analogRead(PIN_CURRENT);
calibCount++;
wdt_reset(); // tetap reset watchdog
if (calibCount >= 80) {
params.currentOffset = calibSum / 80;
saveParams();
#ifdef DEBUG_MODE
DEBUG_PRINT(F("Current offset: "));
DEBUG_PRINTLN(params.currentOffset);
#endif
calibrating = false; // selesai kalibrasi
}
}
}
void startFactoryBrake() {
setMotorRelay(false, false); // matikan semua relay
factoryBrakeStart = millis();
factoryBraking = true;
}
void updateFactoryBrake() {
if (factoryBraking && millis() - factoryBrakeStart >= ACTIVE_BRAKE_DURATION) {
setMotorRelay(false, false); // pastikan relay tetap OFF
factoryBraking = false;
}
}
void factoryMode() {
motorStop();
startCalibrateCurrentOffset(); // mulai kalibrasi non-blocking
jogging = false; factoryBraking = false;
lastJogOpenRaw = HIGH; lastJogCloseRaw = HIGH;
stableOpen = false; stableClose = false;
while (true) {
wdt_reset();
unsigned long now = millis();
heartbeatIndicator(now);
updateBuzzerManager();
updateFactoryBrake();
updateCalibrateCurrentOffset(); // jalankan kalibrasi bertahap
if (digitalRead(PIN_EXIT_F_MODE) == LOW) {
motorStop(); triggerBuzzer(500); break;
}
bool curOpen = digitalRead(PIN_JOG_OPEN) == LOW;
bool curClose = digitalRead(PIN_JOG_CLOSE) == LOW;
bool jogO = getStableButton(curOpen, lastJogOpenRaw, lastJogOpenChange, stableOpen, now);
bool jogC = getStableButton(curClose, lastJogCloseRaw, lastJogCloseChange, stableClose, now);
if (jogO || jogC) {
if (!jogging) {
jogging = true;
jogStartTime = now;
jogDirectionOpen = jogO;
}
if (now - jogStartTime > JOG_TIMEOUT_MS) {
motorStop(); jogging = false; triggerBuzzer(1000);
while (digitalRead(PIN_JOG_OPEN) == LOW || digitalRead(PIN_JOG_CLOSE) == LOW) wdt_reset();
continue;
}
if (jogDirectionOpen && isRainActiveOrRecent()) {
motorStop(); triggerBuzzer(400);
} else {
motorExecute(jogDirectionOpen); // relay ON/OFF, bukan PWM
}
} else if (jogging) {
startFactoryBrake();
jogging = false;
}
}
}
/* ========================= SETUP ========================= */
void setup() {
// Pastikan relay dalam keadaan OFF saat boot
digitalWrite(PIN_RELAY_OPEN, LOW);
digitalWrite(PIN_RELAY_CLOSE, LOW);
// Pastikan buzzer tidak bunyi saat boot
digitalWrite(PIN_BUZZER, HIGH);
Serial.begin(115200);
wdt_enable(WDTO_8S); // aktifkan watchdog 8 detik
#ifdef DEBUG_MODE
DEBUG_PRINT(F("\nJemuran Otomatis v")); DEBUG_PRINTLN(FIRMWARE_VERSION);
DEBUG_PRINT(F("Build: ")); DEBUG_PRINTLN(F(__DATE__ " " __TIME__));
#endif
// Konfigurasi pin input dengan pull-up internal
pinMode(PIN_LS_OPEN, INPUT_PULLUP);
pinMode(PIN_LS_CLOSE, INPUT_PULLUP);
pinMode(PIN_RAIN, INPUT_PULLUP);
pinMode(PIN_FACTORY, INPUT_PULLUP);
pinMode(PIN_JOG_OPEN, INPUT_PULLUP);
pinMode(PIN_JOG_CLOSE, INPUT_PULLUP);
pinMode(PIN_EXIT_F_MODE, INPUT_PULLUP);
// Konfigurasi pin output
pinMode(PIN_LED, OUTPUT);
pinMode(PIN_BUZZER, OUTPUT);
pinMode(PIN_RELAY_OPEN, OUTPUT);
pinMode(PIN_RELAY_CLOSE, OUTPUT);
// Load parameter dari EEPROM
loadParams();
// Jika tombol factory ditekan saat boot, masuk ke mode factory
if (digitalRead(PIN_FACTORY) == LOW) factoryMode();
// Inisialisasi IR receiver
IrReceiver.begin(PIN_IR, ENABLE_LED_FEEDBACK);
#ifdef USE_RTC
// Inisialisasi RTC
if (!rtc.begin()) {
#ifdef DEBUG_MODE
DEBUG_PRINTLN(F("RTC gagal"));
#endif
// Beri buzzer 3 kali sebagai tanda error RTC
for (int i = 0; i < 3; i++) {
digitalWrite(PIN_BUZZER, LOW); delay(100);
digitalWrite(PIN_BUZZER, HIGH); delay(100);
}
} else if (rtc.now().year() < 2025) {
// Jika RTC belum diset, gunakan waktu compile
rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));
#ifdef DEBUG_MODE
DEBUG_PRINTLN(F("RTC diset ke waktu compile"));
#endif
}
#endif
// Ambil state terakhir dari EEPROM
uint8_t s = EEPROM.read(EEPROM_STATE_ADDR);
if (s <= FAULT_LOCKED) currentState = (State)s;
}
/* ========================= LOOP ========================= */
void loop() {
wdt_reset();
handleRemoteIR();
handleAutoSchedule();
handleRainSensor();
runEngine();
updateIndicators();
updateBuzzerManager();
}