/*
* Firmware Jemuran Otomatis
* Versi: 1.2.3
* Build Date: __DATE__ __TIME__
* Author: [Nama Anda atau Tim]
* Deskripsi: Firmware untuk mengontrol motor pembuka/tutup jemuran dengan fitur IR remote,
* sensor hujan, jadwal otomatis via RTC, logging fault ke EEPROM, dan indikator LED/buzzer.
*
* Lisensi: MIT License (atau sesuai kebutuhan produksi)
*
* Catatan Produksi:
* - Debug mode dimatikan untuk hemat resource.
* - Gunakan Arduino IDE untuk compile dan upload ke board AVR (misal ATmega328P).
* - Pastikan library IRremote, EEPROM, RTClib (jika USE_RTC aktif) terinstal.
* - Untuk produksi massal: Gunakan bootloader atau ISP programmer.
* - Tes hardware: IR, sensor hujan, limit switch, RTC (set waktu awal via sketsa RTClib).
*/
#include <Arduino.h>
#include <IRremote.hpp>
#include <EEPROM.h>
#include <avr/wdt.h>
#define USE_RTC // Aktifkan jika modul DS3231 terpasang
#ifdef USE_RTC
#include <Wire.h>
#include "RTClib.h"
RTC_DS3231 rtc;
#endif
// ────────────────────────────────────────────────
// DEBUG CONTROL - dimatikan untuk produksi
// ────────────────────────────────────────────────
#define DEBUG_MODE // comment ini untuk produksi
#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
/* =========================
FIRMWARE VERSION
========================= */
#define FIRMWARE_VERSION "1.2.3"
#define FIRMWARE_BUILD_DATE __DATE__ " " __TIME__
/* =========================
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
// IBT-2
#define PIN_RPWM 9
#define PIN_LPWM 10
#define PIN_REN 7
#define PIN_LEN 8
#define PIN_CURRENT A0
#define PIN_FACTORY A3 // strap to GND for factory mode
/* =========================
IR COMMAND
========================= */
#define IR_OPEN 0xE0
#define IR_CLOSE 0x90
#define IR_STOP 0x68
#define IR_RESET 0xA2
#define IR_LOG 0xE2
/* =========================
EEPROM LAYOUT
========================= */
#define EEPROM_PARAM_ADDR 0
#define EEPROM_STATE_ADDR 64
#define EEPROM_LOG_ADDR 80
#define FAULT_ENTRIES 50
/* =========================
KONFIGURASI MOTOR CONTROL IBT2
========================= */
#define ACTIVE_BRAKE_DURATION 150 // ms, durasi active brake
#define RAMP_STEP 5 // kenaikan PWM tiap interval
#define RAMP_INTERVAL 20 // ms, interval ramp-up
#define RAMP_DOWN_STEP 5 // penurunan PWM tiap interval
#define RAMP_DOWN_INTERVAL 20 // ms, interval ramp-down
/* =========================
STRUCTURES
========================= */
struct ParamBlock {
uint16_t signature;
uint16_t pwmTarget;
uint16_t currentLimit;
uint16_t moveTimeout;
uint16_t rainConfirm;
uint8_t openHour; // jam buka otomatis (0-23)
uint8_t closeHour; // jam tutup otomatis (0-23)
uint8_t crc;
uint8_t flags;
int currentOffset;
};
struct FaultEntry {
uint8_t code;
uint8_t h, m, d, mo, y;
uint8_t crc;
};
ParamBlock params; // global instance
/* =========================
DEFAULT PARAMS
========================= */
const ParamBlock defaultParams PROGMEM = {
0xBEEF, // signature
220, // pwmTarget
600, // currentLimit
25000, // moveTimeout
3000, // rainConfirm
7, // openHour
17, // closeHour
0, // crc
0, // flags
512 // currentOffset
};
/* =========================
STATE MACHINE
========================= */
enum State {
CLOSED,
OPENING,
OPEN,
CLOSING,
STOPPED,
FAULT
};
State currentState = STOPPED;
/* =========================
GLOBALS
========================= */
unsigned long moveStart = 0;
unsigned long lastRamp = 0;
unsigned long rainTimer = 0;
unsigned long lastIR = 0;
unsigned long lastDirectionChange = 0;
inline void motorOpen(int pwm) { motorExecute(pwm, true); }
inline void motorClose(int pwm) { motorExecute(pwm, false); }
const unsigned long DEBOUNCE_TIME_MS = 30UL; // waktu stabil untuk debounce
const unsigned long HEARTBEAT_INTERVAL = 500UL;
const unsigned long BEEP_DURATION = 150UL;
const unsigned long JOG_TIMEOUT_MS = 10000UL;
const unsigned long MOTOR_DEAD_TIME = 300;
// Variabel untuk debounce tombol
unsigned long lastJogOpenChange = 0;
unsigned long lastJogCloseChange = 0;
bool lastJogOpenState = HIGH;
bool lastJogCloseState = HIGH;
// Variabel jog
unsigned long jogStartTime = 0;
bool jogging = false;
bool jogDirectionOpen = false;
const uint8_t FACTORY_PWM = 150;
int pwmNow = 0;
int filteredCurrent = 0; // integer approximation
/* =========================
BUZZER FAULT PATTERN
========================= */
// Definisi pattern SOS: S=short, O=long
enum SosUnit { SHORT, LONG };
const SosUnit sosPattern[9] = {
SHORT, SHORT, SHORT, // S
LONG, LONG, LONG, // O
SHORT, SHORT, SHORT // S
};
// Konstanta timing untuk pola SOS
constexpr unsigned long SOS_SHORT = 200; // durasi dot (ms)
constexpr unsigned long SOS_LONG = 600; // durasi dash (ms)
constexpr unsigned long SOS_GAP = 200; // jeda antar unit (ms)
constexpr unsigned long SOS_REPEAT_GAP = 1000; // jeda antar repeat (ms)
constexpr unsigned long SOS_CYCLE = 300000; // reset siklus tiap 5 menit (ms)
constexpr uint8_t SOS_REPEAT = 3; // jumlah repeat per siklus
/* =========================
LOG THROTTLE
========================= */
#define LOG_FAULT_RATE_LIMIT_MS 3000UL
#define LOG_FAULT_BURST_COUNT 3
#define LOG_SERIAL_COOLDOWN_MS 1500UL
struct LogThrottle {
unsigned long lastLogTime = 0;
uint8_t burstCounter = 0;
unsigned long lastSerialPrint = 0;
} logThrottle;
#define LOG_FAULT(code) do { \
unsigned long now = millis(); \
bool shouldLog = false; \
if (now - logThrottle.lastLogTime > LOG_FAULT_RATE_LIMIT_MS) { \
logThrottle.burstCounter = 0; \
} \
if (logThrottle.burstCounter < LOG_FAULT_BURST_COUNT) { \
shouldLog = true; \
logThrottle.burstCounter++; \
} else if (now - logThrottle.lastLogTime >= LOG_FAULT_RATE_LIMIT_MS) {\
shouldLog = true; \
logThrottle.burstCounter = 1; \
} \
if (shouldLog) { \
logThrottle.lastLogTime = now; \
logFault_nonblocking(code); \
} \
} while(0)
/* =========================
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 PARAM
========================= */
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();
}
}
/* =========================
FAULT LOG
========================= */
void logFault_nonblocking(uint8_t code) {
FaultEntry f;
f.code = code;
// Inisialisasi default agar f tidak berisi data sampah
f.h = f.m = f.d = f.mo = f.y = 0;
#ifdef USE_RTC
// Menggunakan rtc.now() dan memvalidasi tahun sebagai pengganti isrunning()
DateTime nowRTC = rtc.now();
if (nowRTC.year() > 2000 && nowRTC.year() < 2100) {
f.h = nowRTC.hour();
f.m = nowRTC.minute();
f.d = nowRTC.day();
f.mo = nowRTC.month();
f.y = 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; // Validasi index EEPROM
uint16_t addr = EEPROM_LOG_ADDR + 1 + (index * sizeof(FaultEntry));
EEPROM.put(addr, f);
index++;
if (index >= FAULT_ENTRIES) index = 0;
EEPROM.update(EEPROM_LOG_ADDR, index);
unsigned long nowMs = millis();
if (nowMs - logThrottle.lastSerialPrint >= LOG_SERIAL_COOLDOWN_MS) {
logThrottle.lastSerialPrint = nowMs;
#ifdef DEBUG_MODE
DEBUG_PRINT(F("[KERUSAKAN] Kode: "));
DEBUG_PRINT(code);
if (code == 1) DEBUG_PRINT(F(" - Arus motor melebihi batas"));
if (code == 2) DEBUG_PRINT(F(" - Timeout gerakan"));
if (code == 3) DEBUG_PRINT(F(" - Konflik Sensor (LS)"));
DEBUG_PRINT(F(" Waktu: "));
#ifdef USE_RTC
if (f.y > 0) { // Hanya cetak jika data RTC valid
if (f.d < 10) DEBUG_PRINT('0');
DEBUG_PRINT(f.d); DEBUG_PRINT(F("/"));
if (f.mo < 10) DEBUG_PRINT('0');
DEBUG_PRINT(f.mo); DEBUG_PRINT(F("/20"));
DEBUG_PRINT(f.y); DEBUG_PRINT(F(" "));
if (f.h < 10) DEBUG_PRINT('0');
DEBUG_PRINT(f.h); DEBUG_PRINT(F(":"));
if (f.m < 10) DEBUG_PRINT('0');
DEBUG_PRINT(f.m);
} else {
DEBUG_PRINT(F("RTC Offline"));
}
#else
DEBUG_PRINT(F("No RTC"));
#endif
DEBUG_PRINTLN();
#endif
}
}
/* =========================
MOTOR CONTROL IBT2
========================= */
void motorStop(bool activeBrake = false) {
static unsigned long brakeStart = 0;
static bool braking = false;
if (activeBrake && !braking) {
// Mulai active brake non-blocking
digitalWrite(PIN_REN, HIGH);
digitalWrite(PIN_LEN, HIGH);
digitalWrite(PIN_RPWM, HIGH);
digitalWrite(PIN_LPWM, HIGH);
brakeStart = millis();
braking = true;
}
if (braking) {
if (millis() - brakeStart >= ACTIVE_BRAKE_DURATION) {
braking = false;
digitalWrite(PIN_RPWM, LOW);
digitalWrite(PIN_LPWM, LOW);
digitalWrite(PIN_REN, LOW);
digitalWrite(PIN_LEN, LOW);
}
} else {
digitalWrite(PIN_RPWM, LOW);
digitalWrite(PIN_LPWM, LOW);
digitalWrite(PIN_REN, LOW);
digitalWrite(PIN_LEN, LOW);
}
}
inline void motorExecute(int pwm, bool openDir) {
digitalWrite(PIN_REN, HIGH);
digitalWrite(PIN_LEN, HIGH);
if (openDir) {
analogWrite(PIN_RPWM, pwm);
analogWrite(PIN_LPWM, 0);
} else {
analogWrite(PIN_RPWM, 0);
analogWrite(PIN_LPWM, pwm);
}
}
/* =========================
STATE TRANSITION
========================= */
void requestState(State newState) {
if (newState == currentState) return;
if (currentState == FAULT && newState != STOPPED) return;
// Plausibility Check
if (digitalRead(PIN_LS_OPEN) == LOW && digitalRead(PIN_LS_CLOSE) == LOW) {
LOG_FAULT(3); newState = FAULT;
}
// Dead-time triggering
if ((currentState == OPENING && newState == CLOSING) ||
(currentState == CLOSING && newState == OPENING)) {
lastDirectionChange = millis();
motorStop(true);
}
if (newState == OPENING || newState == CLOSING) {
moveStart = millis();
pwmNow = 0;
}
#ifdef DEBUG_MODE
if (newState != currentState) {
DEBUG_PRINT(F("Status berubah ke: "));
switch (newState) {
case CLOSED: DEBUG_PRINTLN(F("Tertutup")); break;
case OPENING: DEBUG_PRINTLN(F("Membuka...")); break;
case OPEN: DEBUG_PRINTLN(F("Terbuka penuh")); break;
case CLOSING: DEBUG_PRINTLN(F("Menutup...")); break;
case STOPPED: DEBUG_PRINTLN(F("Berhenti")); break;
case FAULT: DEBUG_PRINTLN(F("KERUSAKAN - Perlu reset")); break;
}
}
#endif
currentState = newState;
// Optimasi EEPROM write
uint8_t storedState = EEPROM.read(EEPROM_STATE_ADDR);
if (storedState != (uint8_t)currentState) {
EEPROM.update(EEPROM_STATE_ADDR, (uint8_t)currentState);
}
}
/* =========================
ENGINE
========================= */
void runEngine() {
if (millis() - lastDirectionChange < MOTOR_DEAD_TIME) {
motorStop(false);
return;
}
if (currentState == OPENING || currentState == CLOSING) {
// Ramp-up PWM
if (pwmNow < params.pwmTarget && millis() - lastRamp > RAMP_INTERVAL) {
pwmNow = min(pwmNow + RAMP_STEP, params.pwmTarget);
lastRamp = millis();
}
motorExecute(pwmNow, (currentState == OPENING));
// Limit Switches
if ((currentState == OPENING && digitalRead(PIN_LS_OPEN) == LOW) ||
(currentState == CLOSING && digitalRead(PIN_LS_CLOSE) == LOW)) {
motorStop(true);
requestState((currentState == OPENING) ? OPEN : CLOSED);
}
// Current & Timeout Faults
int rawCurrent = abs(analogRead(PIN_CURRENT) - params.currentOffset);
filteredCurrent = (filteredCurrent * 8 + rawCurrent * 2) / 10;
if (millis() - moveStart > 800 && filteredCurrent > params.currentLimit) {
motorStop(true); LOG_FAULT(1); requestState(FAULT);
}
if (millis() - moveStart > params.moveTimeout) {
motorStop(false); LOG_FAULT(2); requestState(FAULT);
}
} else {
// Ramp-down PWM saat berhenti
if (pwmNow > 0 && millis() - lastRamp > RAMP_DOWN_INTERVAL) {
pwmNow = max(pwmNow - RAMP_DOWN_STEP, 0);
lastRamp = millis();
motorExecute(pwmNow, (currentState == OPENING));
} else if (pwmNow == 0) {
motorStop(false);
}
}
}
/* =========================
FACTORY MODE
========================= */
// HELPER FUNCTIONS UNTUK FACTORY MODE
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) {
tone(PIN_BUZZER, 2000, BEEP_DURATION);
} else {
noTone(PIN_BUZZER);
}
}
}
// Buzzer peringatan non-blocking
void beepWarning(unsigned long now, unsigned long duration, unsigned int freq) {
static unsigned long beepStart = 0;
if (beepStart == 0) beepStart = now;
if (now - beepStart < duration) {
tone(PIN_BUZZER, freq);
} else {
noTone(PIN_BUZZER);
beepStart = 0; // reset untuk panggilan berikutnya
}
}
// Debounce tombol non-blocking
bool debounceButton(bool currentState, bool &lastState, unsigned long &lastChange, unsigned long now) {
if (currentState != lastState) {
lastChange = now;
lastState = currentState;
}
return (now - lastChange >= DEBOUNCE_TIME_MS) ? currentState : false;
}
void factoryMode() {
#ifdef DEBUG_MODE
DEBUG_PRINTLN(F("--- MODE FACTORY / DIAGNOSTIK PABRIK ---"));
DEBUG_PRINTLN(F("Tombol JOG_OPEN (A1) → buka motor (debounce 30ms)"));
DEBUG_PRINTLN(F("Tombol JOG_CLOSE (A2) → tutup motor (debounce 30ms)"));
DEBUG_PRINTLN(F("Safety timeout 10 detik"));
DEBUG_PRINTLN(F("Tekan tombol EXIT (D4) untuk keluar dari mode factory"));
#endif
while (true) {
wdt_reset();
unsigned long now = millis();
// Exit condition
if (digitalRead(PIN_EXIT_F_MODE) == LOW) {
#ifdef DEBUG_MODE
DEBUG_PRINTLN(F("Keluar dari MODE FACTORY → kembali ke loop normal"));
#endif
motorStop();
break;
}
// Heartbeat indikator
heartbeatIndicator(now);
// Debounce tombol jog
bool jogOpenStable = debounceButton(digitalRead(PIN_JOG_OPEN) == LOW, lastJogOpenState, lastJogOpenChange, now);
bool jogCloseStable = debounceButton(digitalRead(PIN_JOG_CLOSE) == LOW, lastJogCloseState, lastJogCloseChange, now);
// Logika jog
if (jogOpenStable || jogCloseStable) {
if (!jogging) {
jogging = true;
jogStartTime = now;
jogDirectionOpen = jogOpenStable; // prioritas open
#ifdef DEBUG_MODE
DEBUG_PRINTLN(jogDirectionOpen ? F("JOG BUKA dimulai") : F("JOG TUTUP dimulai"));
#endif
}
// Safety timeout
if (now - jogStartTime > JOG_TIMEOUT_MS) {
#ifdef DEBUG_MODE
DEBUG_PRINTLN(F("JOG TIMEOUT SAFETY - motor stop"));
#endif
motorStop();
jogging = false;
beepWarning(now, 1000, 4000); // buzzer peringatan non-blocking
continue;
}
// Jalankan motor
if (jogDirectionOpen) {
motorOpen(FACTORY_PWM); // gunakan macro untuk speed
} else {
motorClose(FACTORY_PWM);
}
} else {
if (jogging) {
#ifdef DEBUG_MODE
DEBUG_PRINTLN(F("Tombol jog dilepas → motor stop"));
#endif
motorStop();
jogging = false;
}
}
}
}
/* =========================
HELPER FUNCTIONS
========================= */
// Debounce IR berbasis millis()
bool canProcessIR(uint32_t now) {
return (now - lastIR > 400); // gunakan lastIR global yang sudah ada
}
// Cetak satu entry fault dengan aman
void printFaultEntry(uint8_t index, const FaultEntry& f) {
DEBUG_PRINT(F("#")); DEBUG_PRINT(index);
DEBUG_PRINT(F(" Kode:")); DEBUG_PRINT(f.code);
if (f.code == 1) DEBUG_PRINT(F(" (Arus berlebih)"));
else if (f.code == 2) DEBUG_PRINT(F(" (Timeout)"));
else DEBUG_PRINT(F(" (Unknown)"));
#ifdef USE_RTC
DEBUG_PRINT(F(" "));
if (f.y) {
if (f.d < 10) DEBUG_PRINT('0');
DEBUG_PRINT(f.d); DEBUG_PRINT(F("/"));
if (f.mo < 10) DEBUG_PRINT('0');
DEBUG_PRINT(f.mo); DEBUG_PRINT(F("/20"));
DEBUG_PRINT(f.y); DEBUG_PRINT(F(" "));
}
if (f.h < 10) DEBUG_PRINT('0');
DEBUG_PRINT(f.h); DEBUG_PRINT(F(":"));
if (f.m < 10) DEBUG_PRINT('0');
DEBUG_PRINT(f.m);
#endif
DEBUG_PRINTLN();
}
// Safety check: apakah sistem sedang FAULT
bool isFaultLatched() {
return (currentState == FAULT);
}
// Reset FSM dan clear log sederhana
void clearFaultLog() {
EEPROM.update(EEPROM_LOG_ADDR, 0); // reset index log
}
void resetFSM() {
currentState = STOPPED;
}
void handleRemoteIR() {
if (!IrReceiver.decode()) return;
uint32_t now = millis();
if (!canProcessIR(now)) {
IrReceiver.resume();
return;
}
uint8_t cmd = IrReceiver.decodedIRData.command;
#ifdef DEBUG_MODE
DEBUG_PRINT(F("Perintah IR diterima: 0x"));
DEBUG_PRINT(cmd, HEX);
DEBUG_PRINTLN();
#endif
switch (cmd) {
case IR_OPEN:
if (!isFaultLatched()) {
requestState(OPENING);
#ifdef DEBUG_MODE
DEBUG_PRINTLN(F("→ Membuka via IR"));
#endif
}
break;
case IR_CLOSE:
if (!isFaultLatched()) {
requestState(CLOSING);
#ifdef DEBUG_MODE
DEBUG_PRINTLN(F("→ Menutup via IR"));
#endif
}
break;
case IR_STOP:
requestState(STOPPED);
#ifdef DEBUG_MODE
DEBUG_PRINTLN(F("→ STOP via IR"));
#endif
break;
case IR_RESET:
clearFaultLog();
resetFSM();
requestState(STOPPED);
#ifdef DEBUG_MODE
DEBUG_PRINTLN(F("→ RESET via IR"));
#endif
break;
case IR_LOG:
#ifdef DEBUG_MODE
DEBUG_PRINTLN(F("--- INFO FIRMWARE & DAFTAR KERUSAKAN ---"));
DEBUG_PRINT(F("Versi: ")); DEBUG_PRINTLN(F(FIRMWARE_VERSION));
DEBUG_PRINT(F("Build: ")); DEBUG_PRINTLN(F(FIRMWARE_BUILD_DATE));
DEBUG_PRINTLN(F("Daftar Kerusakan:"));
uint8_t printed = 0;
for (uint8_t i = 0; i < FAULT_ENTRIES && printed < 5; i++) {
FaultEntry f;
uint16_t addr = EEPROM_LOG_ADDR + 1 + i * sizeof(FaultEntry);
EEPROM.get(addr, f);
if (crc8((uint8_t*)&f, sizeof(FaultEntry) - 1) == f.crc) {
printFaultEntry(i, f);
printed++;
}
}
if (printed == 5) {
DEBUG_PRINTLN(F("... log dipangkas untuk hemat waktu/memori ..."));
}
DEBUG_PRINTLN(F("-------------------------------------"));
#endif
break;
default:
#ifdef DEBUG_MODE
DEBUG_PRINTLN(F("→ Perintah IR tidak dikenal"));
#endif
break;
}
lastIR = now;
IrReceiver.resume();
}
void handleAutoSchedule() {
#ifdef USE_RTC
DateTime now = rtc.now();
if (now.year() < 2025) return;
static int lastMinuteProcessed = -1;
static bool openMessageSent = false;
static bool closeMessageSent = false;
// Cek apakah kita sudah memproses menit ini
if (now.minute() == lastMinuteProcessed) return;
// Logika Buka Otomatis (Menit ke-0)
if (now.minute() == 0 && now.hour() == params.openHour) {
if (digitalRead(PIN_RAIN) == HIGH) { // Hanya buka jika tidak hujan
requestState(OPENING);
#ifdef DEBUG_MODE
if (!openMessageSent) {
DEBUG_PRINTLN(F("Jadwal otomatis: Membuka pagi"));
openMessageSent = true;
}
#endif
}
lastMinuteProcessed = now.minute();
}
// Logika Tutup Otomatis (Menit ke-0)
else if (now.minute() == 0 && now.hour() == params.closeHour) {
requestState(CLOSING);
#ifdef DEBUG_MODE
if (!closeMessageSent) {
DEBUG_PRINTLN(F("Jadwal otomatis: Menutup sore"));
closeMessageSent = true;
}
#endif
lastMinuteProcessed = now.minute();
}
// Reset flag pesan saat sudah bukan menit ke-0 lagi
if (now.minute() != 0) {
openMessageSent = false;
closeMessageSent = false;
lastMinuteProcessed = -1; // Izinkan proses lagi di jam berikutnya
}
#endif
}
void handleRainSensor() {
static bool lastRainState = HIGH;
static bool rainConfirmMessageSent = false; // ← BARU: flag untuk pesan sekali saja
bool currentRain = digitalRead(PIN_RAIN);
if (currentRain == LOW) {
if (rainTimer == 0) {
rainTimer = millis();
#ifdef DEBUG_MODE
DEBUG_PRINTLN(F("Hujan terdeteksi"));
#endif
rainConfirmMessageSent = false; // reset flag saat hujan baru mulai
}
if (millis() - rainTimer > params.rainConfirm) {
if (currentState != CLOSING && currentState != CLOSED) {
requestState(CLOSING);
#ifdef DEBUG_MODE
if (!rainConfirmMessageSent) {
DEBUG_PRINTLN(F("Konfirmasi hujan → Tutup otomatis"));
rainConfirmMessageSent = true;
}
#endif
}
}
} else {
if (rainTimer != 0 && lastRainState == LOW) {
#ifdef DEBUG_MODE
DEBUG_PRINTLN(F("Hujan berhenti"));
#endif
rainConfirmMessageSent = false; // reset untuk sesi hujan berikutnya
}
rainTimer = 0;
}
lastRainState = currentRain;
}
void updateIndicators() {
static unsigned long lastCycleStart = 0;
static uint8_t sosStep = 0;
static uint8_t sosRepeatCount = 0;
static unsigned long unitStartTime = 0;
static bool unitActive = false;
unsigned long now = millis();
if (currentState == FAULT) {
// LED blink cepat (2 Hz)
static unsigned long lastLedFault = 0;
if (now - lastLedFault >= 250) {
digitalWrite(PIN_LED, !digitalRead(PIN_LED));
lastLedFault = now;
}
// Reset siklus setiap 5 menit
if (now - lastCycleStart >= SOS_CYCLE) {
lastCycleStart = now;
sosStep = 0;
sosRepeatCount = 0;
unitStartTime = now;
unitActive = false;
}
// Jalankan SOS pattern
if (sosRepeatCount < SOS_REPEAT) {
unsigned long duration = (sosPattern[sosStep] == SHORT) ? SOS_SHORT : SOS_LONG;
if (!unitActive) {
// Mulai unit baru
unitStartTime = now;
unitActive = true;
digitalWrite(PIN_BUZZER, LOW); // bunyi (active-LOW)
} else {
if (now - unitStartTime < duration) {
// Masih dalam durasi dot/dash
digitalWrite(PIN_BUZZER, LOW);
} else if (now - unitStartTime < duration + SOS_GAP) {
// Gap antar unit
digitalWrite(PIN_BUZZER, HIGH);
} else {
// Unit selesai, lanjut ke step berikutnya
sosStep++;
unitActive = false;
if (sosStep >= 9) {
sosStep = 0;
sosRepeatCount++;
unitStartTime = now + SOS_REPEAT_GAP; // jeda antar repeat
}
}
}
} else {
digitalWrite(PIN_BUZZER, HIGH); // diam sampai siklus berikutnya
}
} else {
// Normal heartbeat LED (0.5 Hz)
static unsigned long lastLedNormal = 0;
if (now - lastLedNormal >= 500) {
digitalWrite(PIN_LED, !digitalRead(PIN_LED));
lastLedNormal = now;
}
digitalWrite(PIN_BUZZER, HIGH); // diam (active-LOW)
}
}
/* =========================
SETUP
========================= */
void setup() {
Serial.begin(115200);
wdt_enable(WDTO_8S);
#ifdef DEBUG_MODE
DEBUG_PRINTLN();
DEBUG_PRINTLN(F("========================================"));
DEBUG_PRINT(F("Firmware Versi: "));
DEBUG_PRINT(F(FIRMWARE_VERSION));
DEBUG_PRINT(F(" (build: "));
DEBUG_PRINT(F(FIRMWARE_BUILD_DATE));
DEBUG_PRINTLN(F(")"));
DEBUG_PRINTLN(F("========================================"));
DEBUG_PRINTLN(F("Sistem mulai..."));
#endif
// Input limit switch & sensor
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);
// Output indikator & aktuator
pinMode(PIN_LED, OUTPUT);
pinMode(PIN_BUZZER, OUTPUT);
pinMode(PIN_RPWM, OUTPUT);
pinMode(PIN_LPWM, OUTPUT);
pinMode(PIN_REN, OUTPUT);
pinMode(PIN_LEN, OUTPUT);
// Inisialisasi buzzer active-LOW → diam saat HIGH
digitalWrite(PIN_BUZZER, HIGH);
// Factory mode check
if (digitalRead(PIN_FACTORY) == LOW) {
#ifdef DEBUG_MODE
DEBUG_PRINTLN(F("!!! MODE FACTORY AKTIF !!!"));
#endif
factoryMode();
}
// IR receiver
IrReceiver.begin(PIN_IR, ENABLE_LED_FEEDBACK);
#ifdef USE_RTC
bool rtcAvailable = rtc.begin();
if (!rtcAvailable) {
DEBUG_PRINTLN(F("RTC tidak terdeteksi, sistem tetap lanjut tanpa RTC"));
// Jangan ubah currentState ke FAULT, cukup beri warning
// Bisa tambahkan indikator singkat (LED/buzzer) agar teknisi tahu
digitalWrite(PIN_BUZZER, LOW);
delay(100);
digitalWrite(PIN_BUZZER, HIGH);
} else {
DateTime nowCheck = rtc.now();
if (nowCheck.year() < 2025) {
DEBUG_PRINTLN(F("RTC invalid, set ke waktu compile"));
rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));
}
}
#endif
// Load parameter dari EEPROM
loadParams();
#ifdef DEBUG_MODE
DEBUG_PRINT(F("Parameter: PWM=")); DEBUG_PRINT(params.pwmTarget);
DEBUG_PRINT(F(" | Buka=")); DEBUG_PRINT(params.openHour);
DEBUG_PRINT(F(":00 | Tutup=")); DEBUG_PRINT(params.closeHour);
DEBUG_PRINTLN(F(":00"));
#endif
// Status awal dari EEPROM
uint8_t s = EEPROM.read(EEPROM_STATE_ADDR);
if (s <= FAULT) {
currentState = (State)s;
#ifdef DEBUG_MODE
DEBUG_PRINT(F("Status awal: "));
switch (currentState) {
case CLOSED: DEBUG_PRINTLN(F("Tertutup")); break;
case OPENING: DEBUG_PRINTLN(F("Membuka")); break;
case OPEN: DEBUG_PRINTLN(F("Terbuka")); break;
case CLOSING: DEBUG_PRINTLN(F("Menutup")); break;
case STOPPED: DEBUG_PRINTLN(F("Berhenti")); break;
case FAULT: DEBUG_PRINTLN(F("KERUSAKAN"));break;
}
#endif
}
}
/* =========================
LOOP
========================= */
void loop() {
wdt_reset();
handleRemoteIR();
handleAutoSchedule();
handleRainSensor();
runEngine();
updateIndicators();
}