#include <Wire.h>
#include <LiquidCrystal_I2C.h>
// ============================================================
// SMART CLASSROOM ENERGY MANAGEMENT SYSTEM
// Occupancy-based lighting control with IR + PIR sensing
// ============================================================
LiquidCrystal_I2C lcd(0x27, 16, 2);
// --- Pin Definitions ---
const int IR1_PIN = 4;
const int IR2_PIN = 5;
const int PIR_PIN = 27;
const int LED_LOW = 25;
const int LED_MED = 26;
const int LED_HIGH = 18;
// --- Timing Constants ---
const int DEBOUNCE_MS = 200;
const unsigned long SEQ_WINDOW_MS = 1200;
const unsigned long PIR_HOLD_MS = 5000;
const unsigned long FLASH_MS = 1500;
const unsigned long LCD_UPDATE_MS = 200;
// --- Occupancy & Sensor State ---
int occupancy = 0;
int pendingSensor = 0;
unsigned long pendingTime = 0;
unsigned long lastMotionTime = 0;
bool lastIR1 = HIGH;
bool lastIR2 = HIGH;
bool lastPIR = LOW;
unsigned long lastIR1Trigger = 0;
unsigned long lastIR2Trigger = 0;
unsigned long lastLCDUpdate = 0;
// --- LCD Flash State ---
bool flashActive = false;
unsigned long flashStart = 0;
String flashLine1 = "";
String flashLine2 = "";
// ============================================================
// LIGHTING CONTROL
// ============================================================
void setLights(int mode) {
digitalWrite(LED_LOW, mode >= 1 ? HIGH : LOW);
digitalWrite(LED_MED, mode >= 2 ? HIGH : LOW);
digitalWrite(LED_HIGH, mode >= 3 ? HIGH : LOW);
}
// ============================================================
// MODE HELPERS
// ============================================================
String getModeName(int mode) {
switch (mode) {
case 0: return "OFF ";
case 1: return "LOW ";
case 2: return "MED ";
default: return "HIGH";
}
}
int calculateMode() {
if (occupancy == 0) {
return (millis() - lastMotionTime < PIR_HOLD_MS) ? 1 : 0;
}
if (occupancy <= 2) return 1;
if (occupancy <= 4) return 2;
return 3;
}
// ============================================================
// LCD DISPLAY
// ============================================================
// Pads a string to exactly 16 chars to avoid stale characters
String padTo16(String s) {
while (s.length() < 16) s += " ";
return s;
}
void triggerFlash(String line1, String line2) {
flashLine1 = padTo16(line1);
flashLine2 = padTo16(line2);
flashActive = true;
flashStart = millis();
}
void updateLCD(int mode, bool pirState) {
// --- Flash screen for ENTRY / EXIT ---
if (flashActive) {
if (millis() - flashStart < FLASH_MS) {
lcd.setCursor(0, 0);
lcd.print(flashLine1);
lcd.setCursor(0, 1);
lcd.print(flashLine2);
return;
}
flashActive = false;
lcd.clear(); // clean slate after flash ends
}
// --- Throttle normal LCD updates to avoid flicker ---
if (millis() - lastLCDUpdate < LCD_UPDATE_MS) return;
lastLCDUpdate = millis();
// Row 0: "Occ:5 Mode:HIGH"
String row0 = "Occ:" + String(occupancy) + " Mode:" + getModeName(mode);
// Row 1: "Motion: YES"
String row1 = "Motion: " + String(pirState ? "YES" : "NO ");
lcd.setCursor(0, 0);
lcd.print(padTo16(row0));
lcd.setCursor(0, 1);
lcd.print(padTo16(row1));
}
// ============================================================
// ENTRY / EXIT LOGIC
// ============================================================
void handleIR(int sensor) {
unsigned long now = millis();
if (pendingSensor != 0 && (now - pendingTime) <= SEQ_WINDOW_MS) {
if (pendingSensor == 1 && sensor == 2) {
occupancy++;
Serial.println("[EVENT] ENTRY detected | Occupancy: " + String(occupancy));
triggerFlash(" >> ENTRY << ", " Occupancy: " + String(occupancy));
}
else if (pendingSensor == 2 && sensor == 1) {
if (occupancy > 0) occupancy--;
Serial.println("[EVENT] EXIT detected | Occupancy: " + String(occupancy));
triggerFlash(" >> EXIT << ", " Occupancy: " + String(occupancy));
}
pendingSensor = 0;
} else {
pendingSensor = sensor;
pendingTime = now;
}
}
// ============================================================
// SETUP
// ============================================================
void setup() {
Serial.begin(115200);
Serial.println("============================================");
Serial.println(" Smart Classroom Energy Management System ");
Serial.println("============================================");
pinMode(IR1_PIN, INPUT_PULLUP);
pinMode(IR2_PIN, INPUT_PULLUP);
pinMode(PIR_PIN, INPUT_PULLDOWN);
pinMode(LED_LOW, OUTPUT);
pinMode(LED_MED, OUTPUT);
pinMode(LED_HIGH, OUTPUT);
lcd.init();
lcd.backlight();
// Boot message
lcd.setCursor(0, 0);
lcd.print(" Smart Class ");
lcd.setCursor(0, 1);
lcd.print(" Energy System ");
delay(2000);
lcd.clear();
Serial.println("[SYSTEM] Initialisation complete. Ready.");
}
// ============================================================
// MAIN LOOP
// ============================================================
void loop() {
bool ir1 = digitalRead(IR1_PIN);
bool ir2 = digitalRead(IR2_PIN);
bool pir = digitalRead(PIR_PIN);
// --- IR Sensor 1 ---
if (lastIR1 == HIGH && ir1 == LOW && (millis() - lastIR1Trigger > DEBOUNCE_MS)) {
lastIR1Trigger = millis();
handleIR(1);
}
// --- IR Sensor 2 ---
if (lastIR2 == HIGH && ir2 == LOW && (millis() - lastIR2Trigger > DEBOUNCE_MS)) {
lastIR2Trigger = millis();
handleIR(2);
}
// --- PIR Sensor ---
if (lastPIR == LOW && pir == HIGH) {
lastMotionTime = millis();
Serial.println("[EVENT] PIR motion detected");
}
// --- Reset incomplete IR sequence ---
if (pendingSensor != 0 && (millis() - pendingTime > SEQ_WINDOW_MS)) {
pendingSensor = 0;
}
// --- Update outputs ---
int mode = calculateMode();
setLights(mode);
updateLCD(mode, pir);
// --- Serial status (every loop) ---
Serial.print("[STATUS] Occ:");
Serial.print(occupancy);
Serial.print(" | Mode:");
Serial.print(getModeName(mode));
Serial.print(" | Motion:");
Serial.println(pir ? "YES" : "NO");
lastIR1 = ir1;
lastIR2 = ir2;
lastPIR = pir;
delay(100);
}