#include <IRremote.hpp>
#include <Wire.h>
#include "RTClib.h"
#include <avr/wdt.h>
#include <EEPROM.h>
/* ==============================
Industrial High-End Roof Controller
- Modular, audit-ready, non-blocking
- EEPROM.update() for wear-leveling
- RTC timestamp logging (H:M:D)
- Smooth motor ramp-up/down
- IR + automation control
- Ready for mass deployment
============================== */
#define LOG_INFO(msg) Serial.println(F(msg))
#define LOG_WARN(msg) Serial.println(F(msg))
#define LOG_ERROR(msg) Serial.println(F(msg))
enum State {IDLE, OPENING, CLOSING, FAULT};
State currentState = IDLE;
State resumeState = IDLE;
/* --- PIN CONFIG --- */
const int RECV_PIN = 2;
const int RPWM = 9;
const int LPWM = 10;
const int REN = 8;
const int LEN = 7;
const int LIMIT_OPEN = 5;
const int LIMIT_CLOSE = 6;
const int RAIN_SENSOR = 4;
const int LDR_SENSOR = A2;
const int ESTOP = A1;
const int BUZZER = 12;
const int RESET_FAULT = 3;
const int LED_HEART = 13;
const int CURRENT_SENSOR = A0;
/* --- IR Remote Codes (NEC) --- */
#define CMD_OPEN 0x62
#define CMD_CLOSE 0xA8
#define CMD_STOP 0xA2
#define CMD_LOG 0xC3
/* --- EEPROM & Logging --- */
#define FAULT_ENTRIES 100
#define EEPROM_INDEX_START 0
#define EEPROM_INDEX_SIZE 16
#define EEPROM_LOG_START 20
#define EEPROM_LOG_SIZE (FAULT_ENTRIES*5) // code + hr + min + day + month
struct FaultEntry {
byte code;
byte hour;
byte minute;
byte day;
byte month;
};
uint16_t faultLogIndex = 0;
byte idxPos = 0;
unsigned long lastFaultWrite = 0;
/* --- System Variables --- */
RTC_DS3231 rtc;
bool faultLatched = false;
bool rainActive = false;
unsigned long lastBlink = 0;
bool ledState = LOW;
const int CURRENT_LIMIT = 500;
float filteredCurrent = 0.0;
float currentOffset = 2.5;
unsigned long overCurrentStart = 0, graceStart = 0;
int lastRTCDay = -1;
unsigned long lastLdrAction = 0;
#define ADC_REF 5.0
unsigned long openStart=0, closeStart=0;
long calibrationSum=0; int calibrationCount=0;
const int calibrationSamples=100;
bool buzzerOn=false; unsigned long buzzerStart=0, buzzerDuration=0;
unsigned long lastLogCmd=0;
/* ===============================
HELPER FUNCTIONS
=============================== */
void saveFaultIndex(){
int addr=EEPROM_INDEX_START+idxPos;
EEPROM.update(addr,faultLogIndex&0xFF);
EEPROM.update(addr+1,(faultLogIndex>>8)&0xFF);
idxPos=(idxPos+2)%EEPROM_INDEX_SIZE;
}
void buzzerBeep(unsigned long d){
buzzerOn=true;
buzzerStart=millis();
buzzerDuration=d;
digitalWrite(BUZZER,HIGH);
}
void buzzerUpdate(){
if(buzzerOn && millis()-buzzerStart>=buzzerDuration){
digitalWrite(BUZZER,LOW);
buzzerOn=false;
}
}
bool readStable(int pin,int expected,unsigned long dur=50){
static unsigned long lastChange[20]={0};
static int lastState[20]={HIGH};
int idx=pin; int state=digitalRead(pin);
if(state!=lastState[idx]){lastChange[idx]=millis();lastState[idx]=state;}
if((millis()-lastChange[idx])>=dur && state==expected) return true;
return false;
}
/* ===============================
SETUP
=============================== */
void setup(){
Serial.begin(115200);
IrReceiver.begin(RECV_PIN,ENABLE_LED_FEEDBACK);
rtc.begin();
pinMode(RPWM,OUTPUT); pinMode(LPWM,OUTPUT);
pinMode(REN,OUTPUT); pinMode(LEN,OUTPUT);
pinMode(LIMIT_OPEN,INPUT_PULLUP); pinMode(LIMIT_CLOSE,INPUT_PULLUP);
pinMode(RAIN_SENSOR,INPUT_PULLUP); pinMode(ESTOP,INPUT_PULLUP);
pinMode(BUZZER,OUTPUT); pinMode(RESET_FAULT,INPUT_PULLUP); pinMode(LED_HEART,OUTPUT);
stopMotor();
LOG_INFO("[INFO] System startup...");
graceStart=millis();
wdt_enable(WDTO_8S);
// Restore last fault log index
byte low=EEPROM.read(EEPROM_INDEX_START);
byte high=EEPROM.read(EEPROM_INDEX_START+1);
faultLogIndex=(high<<8)|low;
}
/* ===============================
MAIN LOOP
=============================== */
void loop(){
wdt_reset();
buzzerUpdate();
ledIndicator();
if(readStable(ESTOP,LOW)){
if(currentState!=FAULT){ faultTrip(1); setState(FAULT); }
return;
}
bool rainDetected = readStable(RAIN_SENSOR,LOW);
if(rainDetected){
if(currentState==IDLE && readStable(LIMIT_CLOSE,HIGH)){ setState(CLOSING); resumeState=OPENING; }
rainActive = true;
} else if(rainActive){
rainActive=false;
if(resumeState!=IDLE){
int ldr = analogRead(LDR_SENSOR);
if((resumeState==OPENING && ldr>500)||(resumeState==CLOSING && ldr<500)){
LOG_INFO("[INFO] Rain cleared → Resuming");
setState(resumeState);
}
resumeState=IDLE;
}
}
if(readStable(RESET_FAULT,LOW) && currentState==FAULT){ resetFault(); return; }
if(currentState==FAULT) return;
if(IrReceiver.decode()){ handleIR(IrReceiver.decodedIRData.command); IrReceiver.resume(); }
handleAutomation();
switch(currentState){
case OPENING:
if(readStable(LIMIT_OPEN,LOW)){ setState(IDLE); rampDownMotorNB(); }
else{ rampMotor(true); checkCurrent(); checkLimitFault(); }
break;
case CLOSING:
if(readStable(LIMIT_CLOSE,LOW)){ setState(IDLE); rampDownMotorNB(); }
else{ rampMotor(false); checkCurrent(); checkLimitFault(); }
break;
case IDLE: stopMotor(); break;
case FAULT: stopMotor(); break;
}
calibrateCurrentSensorAsync();
}
/* ===============================
STATE MANAGEMENT
=============================== */
void setState(State newState){
if(faultLatched && newState!=FAULT) return;
if(currentState!=newState){
currentState=newState; LOG_INFO("[INFO] State changed");
digitalWrite(BUZZER,LOW); buzzerOn=false;
if(newState==OPENING||newState==CLOSING) graceStart=millis();
if(newState==IDLE){ openStart=0; closeStart=0; }
}
}
/* ===============================
IR HANDLER
=============================== */
void handleIR(uint8_t cmd){
if(rainActive && cmd==CMD_OPEN){ LOG_WARN("[WARN] OPEN blocked due to rain"); return; }
if(cmd==CMD_OPEN){ setState(OPENING); resumeState=OPENING; }
else if(cmd==CMD_CLOSE){ setState(CLOSING); resumeState=CLOSING; }
else if(cmd==CMD_STOP){ rampDownMotorNB(); setState(IDLE); }
else if(cmd==CMD_LOG){
if(millis()-lastLogCmd>500){
lastLogCmd=millis();
LOG_INFO("[INFO] Printing last 20 faults...");
digitalWrite(LED_HEART,HIGH);
printFaultLogRange(20);
buzzerBeep(200);
digitalWrite(LED_HEART,LOW);
}
}
}
/* ===============================
AUTOMATION
=============================== */
void handleAutomation(){
DateTime now = rtc.now(); int ldr=analogRead(LDR_SENSOR);
if(now.hour()==6 && now.minute()==0 && now.day()!=lastRTCDay){
if(!rainActive){ setState(OPENING); resumeState=OPENING; graceStart=millis(); }
lastRTCDay=now.day();
}
if(currentState==IDLE && !rainActive && millis()-lastLdrAction>300000){
if(ldr>700 && readStable(LIMIT_OPEN,HIGH)){ setState(OPENING); lastLdrAction=millis(); graceStart=millis(); resumeState=OPENING; }
else if(ldr<300 && readStable(LIMIT_CLOSE,HIGH)){ setState(CLOSING); lastLdrAction=millis(); graceStart=millis(); resumeState=CLOSING; }
}
}
/* ===============================
MOTOR CONTROL
=============================== */
void rampMotor(bool opening){
static int pwmVal=0; static State lastState=IDLE;
if(currentState!=lastState) pwmVal=0; lastState=currentState;
if(pwmVal<200) pwmVal+=10;
digitalWrite(REN,HIGH); digitalWrite(LEN,HIGH);
if(opening){ analogWrite(RPWM,pwmVal); analogWrite(LPWM,0); }
else{ analogWrite(RPWM,0); analogWrite(LPWM,pwmVal); }
}
void rampDownMotorNB(){
static int pwmVal=200; static unsigned long lastStep=0; static State lastState=IDLE;
if(currentState!=lastState){ pwmVal=200; lastState=currentState; }
if(pwmVal>0 && millis()-lastStep>=50){
pwmVal-=10; if(pwmVal<0) pwmVal=0;
analogWrite(RPWM,pwmVal); analogWrite(LPWM,pwmVal);
lastStep=millis();
}
if(pwmVal<=0){ stopMotor(); overCurrentStart=0; }
}
void stopMotor(){ digitalWrite(REN,LOW); digitalWrite(LEN,LOW); analogWrite(RPWM,0); analogWrite(LPWM,0); }
/* ===============================
FAULT HANDLING
=============================== */
void logFault(byte code){
if(millis()-lastFaultWrite<60000) return;
int addr = EEPROM_LOG_START + ((faultLogIndex % FAULT_ENTRIES) * 5);
DateTime now = rtc.now();
FaultEntry entry;
entry.code = code;
entry.hour = now.hour();
entry.minute = now.minute();
entry.day = now.day();
entry.month = now.month();
EEPROM.update(addr, entry.code);
EEPROM.update(addr+1, entry.hour);
EEPROM.update(addr+2, entry.minute);
EEPROM.update(addr+3, entry.day);
EEPROM.update(addr+4, entry.month);
faultLogIndex++;
saveFaultIndex();
lastFaultWrite = millis();
Serial.print(F("[FAULT] Code ")); Serial.print(code);
Serial.print(F(" at ")); Serial.print(entry.hour);
Serial.print(F(":")); Serial.print(entry.minute);
Serial.print(F(" ")); Serial.print(entry.day);
Serial.print(F("/")); Serial.println(entry.month);
}
void faultTrip(byte code){
if(!faultLatched){ LOG_ERROR("[ERROR] FAULT latched"); logFault(code); }
stopMotor();
faultLatched=true;
buzzerBeep(1000);
}
void resetFault(){
faultLatched=false;
setState(IDLE);
buzzerBeep(100);
LOG_INFO("[INFO] FAULT reset → System ready");
resetCalibration();
}
/* ===============================
LED HEARTBEAT
=============================== */
void ledIndicator(){
unsigned long now = millis();
unsigned long interval = 1000;
if(currentState==OPENING||currentState==CLOSING) interval=500;
else if(currentState==FAULT) interval=200;
if(now-lastBlink>=interval){ ledState=!ledState; digitalWrite(LED_HEART,ledState); lastBlink=now; }
}
/* ===============================
CURRENT SENSOR
=============================== */
float readCurrentRaw(){
int raw = analogRead(CURRENT_SENSOR);
float voltage = (raw/1023.0)*ADC_REF;
return (voltage-currentOffset)/0.066;
}
void resetCalibration(){ calibrationSum=0; calibrationCount=0; }
void calibrateCurrentSensorAsync(){
if(currentState==OPENING||currentState==CLOSING) return;
if(calibrationCount<calibrationSamples){
calibrationSum += analogRead(CURRENT_SENSOR);
calibrationCount++;
}
else if(calibrationCount==calibrationSamples){
int raw = calibrationSum/calibrationSamples;
float voltage = (raw/1023.0)*ADC_REF;
currentOffset = (voltage<2.0 || voltage>3.0) ? 2.5 : voltage;
LOG_INFO("[INFO] Current sensor offset calibrated");
calibrationCount++;
}
}
void checkCurrent(){
float current = readCurrentRaw();
float alpha = 0.2;
filteredCurrent = alpha*current + (1-alpha)*filteredCurrent;
if(millis()-graceStart<2000) return;
if(abs(filteredCurrent)*1000 > CURRENT_LIMIT){
if(overCurrentStart==0) overCurrentStart=millis();
if(millis()-overCurrentStart>200){
LOG_ERROR("[ERROR] Overcurrent → FAULT");
faultTrip(2);
setState(FAULT);
}
} else overCurrentStart=0;
}
void checkLimitFault(){
if(currentState==OPENING){
if(openStart==0) openStart=millis();
if(readStable(LIMIT_OPEN,HIGH) && millis()-openStart>60000){
LOG_ERROR("[ERROR] Limit OPEN timeout → FAULT");
faultTrip(3);
setState(FAULT);
}
} else openStart=0;
if(currentState==CLOSING){
if(closeStart==0) closeStart=millis();
if(readStable(LIMIT_CLOSE,HIGH) && millis()-closeStart>60000){
LOG_ERROR("[ERROR] Limit CLOSE timeout → FAULT");
faultTrip(4);
setState(FAULT);
}
} else closeStart=0;
}
/* ===============================
FAULT LOG PRINTING
=============================== */
void printFaultLogRange(int count){
int start = max(0,(int)faultLogIndex-count);
for(int i=start;i<faultLogIndex;i++){
int addr = EEPROM_LOG_START + ((i % FAULT_ENTRIES)*5);
byte code = EEPROM.read(addr);
byte hour = EEPROM.read(addr+1);
byte minute = EEPROM.read(addr+2);
byte day = EEPROM.read(addr+3);
byte month = EEPROM.read(addr+4);
if(code!=0xFF){
Serial.print(F("Idx ")); Serial.print(i);
Serial.print(F(" → FAULT code: "));
switch(code){
case 1: Serial.print(F("Emergency Stop")); break;
case 2: Serial.print(F("Overcurrent")); break;
case 3: Serial.print(F("Limit OPEN timeout")); break;
case 4: Serial.print(F("Limit CLOSE timeout")); break;
default: Serial.print(code); break;
}
Serial.print(F(" at ")); Serial.print(hour);
Serial.print(F(":")); Serial.print(minute);
Serial.print(F(" ")); Serial.print(day);
Serial.print(F("/")); Serial.println(month);
}
}
}