// sketch.ino
#include <Arduino.h>
#include <Adafruit_GFX.h> // Core graphics library
#include <Adafruit_ILI9341.h> // Hardware-specific library for ILI9341
#include <SPI.h> // SPI communication for the display
#include <ESP32Servo.h> // Servo library for ESP32
#include <IRremote.h> // NEW: For IR receiver (generic IRremote library)
// --- DISPLAY PINS (ILI9341) ---
// These are standard SPI pins for ESP32 with the ILI9341, check your diagram for actual connections.
#define TFT_CS 14 // Chip Select pin
#define TFT_DC 13 // Data/Command pin
#define TFT_RST 12 // Reset pin
#define TFT_MOSI 23 // MOSI pin (Standard SPI MOSI for ESP32)
#define TFT_SCLK 18 // SCK pin (Standard SPI SCLK for ESP32)
// --- SENSOR PINS ---
#define GAS_AOUT_PIN 35 // Gas sensor Analog Out (MQ-series typically uses A0, connected to ESP32 ADC1_CH7)
#define GAS_DOUT_PIN 17 // Gas sensor Digital Out
#define TMP36_PIN 34 // TMP36 Analog Out (Connected to ESP32 ADC1_CH6)
// --- OUTPUT PINS ---
#define BUZZER_PIN 16 // Buzzer
#define RELAY_PIN 33 // Relay module IN pin (Controls the relay, HIGH to activate)
#define RGB_LED_R_PIN 19 // RGB LED Red
#define RGB_LED_G_PIN 5 // RGB LED Green
#define RGB_LED_B_PIN 2 // RGB LED Blue
// --- INPUT PINS ---
#define RED_BTN_PIN 25 // Red Pushbutton (Manual Extinguisher)
#define GREEN_BTN_PIN 26 // Green Pushbutton (Reset/Arm)
#define SLIDE_SW_PIN 32 // Slide Switch (Arm/Disarm)
#define IR_RECEIVE_PIN 27 // IR Receiver Data Pin
// --- ROTARY ENCODER PINS ---
#define ENCODER_CLK_PIN 15 // CLK
#define ENCODER_DT_PIN 21 // DT
#define ENCODER_SW_PIN 22 // SW (Button)
// --- SERVO PINS ---
#define SERVO1_PIN 4 // Servo 1
#define SERVO2_PIN 39 // Servo 2 (ESP32's VP pin is GPIO39, an ADC pin, but can be used as PWM for Servo)
// --- THRESHOLDS & TIMERS ---
#define GAS_THRESHOLD_ANALOG 1500 // Analog threshold for gas detection. Adjust based on your sensor and environment.
#define GAS_THRESHOLD_DIGITAL LOW // Digital output of MQ sensors is typically LOW when gas is detected.
#define FIRE_TEMP_THRESHOLD 60.0 // Temperature in Celsius for fire alert (initial warning)
#define EXTINGUISHER_TEMP_THRESHOLD 80.0 // Temperature in Celsius to activate extinguisher (critical action)
#define TEMP_HYSTERESIS_C 5.0 // Hysteresis for temperature thresholds to prevent rapid toggling
#define GAS_HYSTERESIS_ANALOG 100 // Hysteresis for analog gas reading
#define DEBOUNCE_DELAY_MS 50 // Button debounce time in milliseconds
#define DISPLAY_UPDATE_INTERVAL_MS 500 // How often to update the display to prevent flickering
#define SENSOR_READ_INTERVAL_MS 100 // How often to read sensors
#define BUZZER_INTERVAL_MS 250 // Buzzer blink interval for alert (faster for higher urgency)
// --- GLOBAL VARIABLES ---
Adafruit_ILI9341 tft = Adafruit_ILI9341(&SPI, TFT_CS, TFT_DC, TFT_RST);
Servo servo1;
Servo servo2;
IRrecv irrecv(IR_RECEIVE_PIN);
decode_results results; // Stores IR decode results
volatile bool gasDetected = false;
volatile bool fireDetected = false;
volatile bool extinguisherActive = false;
volatile bool systemArmed = true; // True if armed (can detect), false if disarmed
long lastDisplayUpdate = 0;
long lastSensorRead = 0;
long lastBuzzerToggle = 0;
// Encoder variables (can be used for settings, volume, etc., not critical for core function right now)
volatile int encoderPos = 0;
volatile int lastEncoderCLKState = LOW; // Stores previous CLK state for edge detection
volatile unsigned long lastEncoderBtnPress = 0;
// --- FUNCTION PROTOTYPES ---
void setupPins();
void setupDisplay();
void displayStatus(const char* line1, const char* line2, uint16_t color1, uint16_t color2);
void setRGBLED(int r, int g, int b);
void readSensors();
void handleBuzzer();
void handleInputs();
void handleServos();
void handleIRRemote();
void handleRotaryEncoder();
// --- SETUP ---
void setup() {
Serial.begin(115200); // Start serial communication for debugging
Serial.println("Welcome to EMBERQUENCHED! Initializing...");
setupPins(); // Configure all GPIO pins
setupDisplay(); // Initialize the ILI9341 display
servo1.attach(SERVO1_PIN); // Attach servo objects to their pins
servo2.attach(SERVO2_PIN);
servo1.write(0); // Set initial position (e.g., closed/off)
servo2.write(0);
irrecv.enableIRIn(); // Start the IR receiver
// Initial display message
displayStatus("Welcome to", "EMBERQUENCHED!", ILI9341_WHITE, ILI9341_GREEN);
setRGBLED(0, 255, 0); // Green for "initializing" / "safe"
delay(2000); // Show welcome message for 2 seconds
}
// --- LOOP ---
void loop() {
unsigned long currentMillis = millis(); // Get current time for non-blocking operations
// Read sensors periodically
if (currentMillis - lastSensorRead >= SENSOR_READ_INTERVAL_MS) {
readSensors();
lastSensorRead = currentMillis;
}
// Update display periodically based on system state
if (currentMillis - lastDisplayUpdate >= DISPLAY_UPDATE_INTERVAL_MS) {
if (!systemArmed) {
displayStatus("SYSTEM DISARMED", "No protection!", ILI9341_BLUE, ILI9341_CYAN);
setRGBLED(0, 0, 255); // Blue for disarmed
} else if (extinguisherActive) {
displayStatus("FIRE EMERGENCY", "EXTINGUISHER ACT", ILI9341_RED, ILI9341_YELLOW);
setRGBLED(255, 0, 0); // Solid Red for extinguisher active
} else if (fireDetected) {
displayStatus("🔥 FIRE ALERT!", "VACATE Building!", ILI9341_RED, ILI9341_ORANGE);
setRGBLED(255, 165, 0); // Orange for fire warning
} else if (gasDetected) {
displayStatus("SMOKE DETECTED!", "VACATE Building!", ILI9341_RED, ILI9341_MAGENTA);
setRGBLED(255, 255, 0); // Yellow for smoke detection
} else {
displayStatus("SYSTEM SAFE", "No Fire / Gas!", ILI9341_GREEN, ILI9341_CYAN);
setRGBLED(0, 255, 0); // Green for safe
}
lastDisplayUpdate = currentMillis;
}
// Handle other components non-blocking
handleBuzzer();
handleInputs();
handleIRRemote();
handleRotaryEncoder(); // Can be used for future features like settings or brightness
handleServos();
}
// --- FUNCTION IMPLEMENTATIONS ---
void setupPins() {
// Sensor Inputs
pinMode(GAS_DOUT_PIN, INPUT); // Digital input for gas sensor DOUT
// Analog pins (GAS_AOUT_PIN, TMP36_PIN) are implicitly set as inputs when analogRead() is called.
// Output Pins
pinMode(BUZZER_PIN, OUTPUT);
digitalWrite(BUZZER_PIN, LOW); // Ensure buzzer is off initially
pinMode(RELAY_PIN, OUTPUT);
digitalWrite(RELAY_PIN, LOW); // Ensure relay is off initially (NO contact open, NC closed)
pinMode(RGB_LED_R_PIN, OUTPUT);
pinMode(RGB_LED_G_PIN, OUTPUT);
pinMode(RGB_LED_B_PIN, OUTPUT);
setRGBLED(0, 0, 0); // Turn off RGB LED initially
// Input Pins with Pull-up resistors (for buttons and switch)
pinMode(RED_BTN_PIN, INPUT_PULLUP);
pinMode(GREEN_BTN_PIN, INPUT_PULLUP);
pinMode(SLIDE_SW_PIN, INPUT_PULLUP); // Assuming switch connects to GND when "ON"
pinMode(IR_RECEIVE_PIN, INPUT); // IR receiver data pin
// Rotary Encoder Pins with Pull-up resistors
pinMode(ENCODER_CLK_PIN, INPUT_PULLUP);
pinMode(ENCODER_DT_PIN, INPUT_PULLUP);
pinMode(ENCODER_SW_PIN, INPUT_PULLUP); // Encoder button
lastEncoderCLKState = digitalRead(ENCODER_CLK_PIN); // Initialize last state for encoder
}
void setupDisplay() {
tft.begin();
tft.setRotation(1); // Adjust if your display is oriented differently (0-3). 1 is usually landscape.
tft.fillScreen(ILI9341_BLACK); // Clear screen
tft.setTextWrap(true); // Allow text to wrap to the next line
tft.setTextSize(3); // Set default text size
}
// Function to update the TFT display with two lines of text and custom colors
void displayStatus(const char* line1, const char* line2, uint16_t color1, uint16_t color2) {
tft.fillScreen(ILI9341_BLACK); // Clear the screen each time to prevent text overlap
tft.setCursor(10, 50); // Set cursor for the first line
tft.setTextColor(color1); // Set color for the first line
tft.print(line1); // Print the first line
tft.setCursor(10, 100); // Set cursor for the second line
tft.setTextColor(color2); // Set color for the second line
tft.print(line2); // Print the second line
}
// Function to control the RGB LED (assuming common anode, so 0 is brightest, 255 is off)
void setRGBLED(int r, int g, int b) {
analogWrite(RGB_LED_R_PIN, 255 - r);
analogWrite(RGB_LED_G_PIN, 255 - g);
analogWrite(RGB_LED_B_PIN, 255 - b);
}
void readSensors() {
// If system is disarmed by the switch, clear all detection flags and turn off outputs
if (!systemArmed) {
gasDetected = false;
fireDetected = false;
extinguisherActive = false;
digitalWrite(RELAY_PIN, LOW);
digitalWrite(BUZZER_PIN, LOW);
return; // Exit early, no sensor reading needed if disarmed
}
// --- Read Gas Sensor ---
int gasAnalog = analogRead(GAS_AOUT_PIN);
bool gasDigital = (digitalRead(GAS_DOUT_PIN) == GAS_THRESHOLD_DIGITAL); // True if digital threshold met
// --- Read TMP36 Temperature Sensor ---
// ESP32 ADC reads 0-4095 for 0-3.3V (approx).
// TMP36: Vout = (Temp_C * 0.01) + 0.5V
// Temp_C = (Vout - 0.5) / 0.01
float voltage = (float)analogRead(TMP36_PIN) * (3.3 / 4095.0); // Convert ADC reading to voltage
float temperatureC = (voltage - 0.5) * 100.0; // Convert voltage to Celsius
Serial.print("[Sensor] Temp: ");
Serial.print(temperatureC, 2); // Print with 2 decimal places
Serial.print(" C, Gas Analog: ");
Serial.print(gasAnalog);
Serial.print(", Gas Digital: ");
Serial.println(gasDigital ? "DETECTED" : "CLEAR");
// --- Update Alert Flags based on readings ---
bool currentGasDetected = (gasAnalog > GAS_THRESHOLD_ANALOG || gasDigital);
bool currentFireDetected = (temperatureC > FIRE_TEMP_THRESHOLD);
bool currentExtinguisherActive = (temperatureC > EXTINGUISHER_TEMP_THRESHOLD);
// Apply hysteresis for gas detection
if (currentGasDetected) {
gasDetected = true; // Once detected, stays detected until cleared by reset or system disarm
} else if (gasAnalog < (GAS_THRESHOLD_ANALOG - GAS_HYSTERESIS_ANALOG) && !gasDigital) {
gasDetected = false; // Only clear if well below threshold and digital is clear
}
// Apply hysteresis for fire detection
if (currentFireDetected) {
fireDetected = true;
} else if (temperatureC < (FIRE_TEMP_THRESHOLD - TEMP_HYSTERESIS_C)) {
fireDetected = false;
}
// Apply hysteresis for extinguisher activation
if (currentExtinguisherActive) {
extinguisherActive = true;
} else if (temperatureC < (EXTINGUISHER_TEMP_THRESHOLD - TEMP_HYSTERESIS_C)) {
extinguisherActive = false;
}
// Control Relay based on extinguisher state
digitalWrite(RELAY_PIN, extinguisherActive ? HIGH : LOW); // HIGH to activate relay
}
// Manages the buzzer's on/off state based on alert conditions
void handleBuzzer() {
unsigned long currentMillis = millis(); // Get current time for non-blocking operations
// Buzzer behavior based on alert levels
if (systemArmed && (fireDetected || gasDetected)) {
if (currentMillis - lastBuzzerToggle >= BUZZER_INTERVAL_MS) {
lastBuzzerToggle = currentMillis;
if (digitalRead(BUZZER_PIN) == LOW) {
digitalWrite(BUZZER_PIN, HIGH); // Turn buzzer ON
} else {
digitalWrite(BUZZER_PIN, LOW); // Turn buzzer OFF
}
}
} else {
digitalWrite(BUZZER_PIN, LOW); // Turn buzzer off if no alert or disarmed
}
}
void handleInputs() {
// Read slide switch for arm/disarm
// Assuming switch connects to GND when ON (active LOW)
systemArmed = (digitalRead(SLIDE_SW_PIN) == HIGH); // HIGH means armed (switch off/open)
// Red button: Manual extinguisher activation (only if armed)
if (systemArmed && digitalRead(RED_BTN_PIN) == LOW) { // Button pressed (active LOW)
static unsigned long lastBtnPress = 0;
if (millis() - lastBtnPress > DEBOUNCE_DELAY_MS) {
extinguisherActive = true; // Force extinguisher on
Serial.println("[Input] Red Button: Manual Extinguisher ACTIVATED!");
lastBtnPress = millis();
}
}
// Green button: Reset/Disarm
if (digitalRead(GREEN_BTN_PIN) == LOW) { // Button pressed (active LOW)
static unsigned long lastBtnPress = 0;
if (millis() - lastBtnPress > DEBOUNCE_DELAY_MS) {
// If an alert is active, pressing green button clears alerts
if (fireDetected || gasDetected || extinguisherActive) {
fireDetected = false;
gasDetected = false;
extinguisherActive = false;
digitalWrite(RELAY_PIN, LOW); // Turn off relay
digitalWrite(BUZZER_PIN, LOW); // Turn off buzzer
setRGBLED(0, 255, 0); // Back to green
Serial.println("[Input] Green Button: System Reset/Alerts Cleared.");
} else {
// If no alert, toggles arm/disarm state for convenience (optional)
systemArmed = !systemArmed;
Serial.print("[Input] Green Button: System ");
Serial.println(systemArmed ? "ARMED" : "DISARMED");
}
lastBtnPress = millis();
}
}
}
void handleServos() {
// Example: Servo 1 activates with extinguisher
if (extinguisherActive) {
servo1.write(90); // Move to 90 degrees (e.g., open extinguisher valve)
} else {
servo1.write(0); // Move to 0 degrees (e.g., close valve)
}
// Example: Servo 2 could be controlled by encoder or IR remote
// For now, keep it simple, or make it react to a state
if (systemArmed) {
servo2.write(45); // Example: Servo 2 to 45 degrees when armed
} else {
servo2.write(0);
}
}
void handleIRRemote() {
if (irrecv.decode(&results)) {
Serial.print("[IR] Received: ");
// For IRremote library, results.value directly holds the decoded data
Serial.print(results.value, HEX);
Serial.println("");
if (results.value == 0xFFA25D) { // Example: Power button on a common IR remote
systemArmed = !systemArmed;
Serial.print("[IR] System Toggled: ");
Serial.println(systemArmed ? "ARMED" : "DISARMED");
} else if (results.value == 0xFF629D) { // Example: Play/Pause button
if (fireDetected || gasDetected || extinguisherActive) {
fireDetected = false;
gasDetected = false;
extinguisherActive = false;
digitalWrite(RELAY_PIN, LOW);
digitalWrite(BUZZER_PIN, LOW);
setRGBLED(0, 255, 0);
Serial.println("[IR] Alerts Cleared.");
}
}
irrecv.resume(); // Receive the next value
}
}
void handleRotaryEncoder() {
int newCLKState = digitalRead(ENCODER_CLK_PIN);
if (newCLKState != lastEncoderCLKState) {
// A change in CLK state means rotation
if (digitalRead(ENCODER_DT_PIN) != newCLKState) { // Check DT state
encoderPos++; // Clockwise
} else {
encoderPos--; // Counter-clockwise
}
Serial.print("[Encoder] Position: ");
Serial.println(encoderPos);
}
lastEncoderCLKState = newCLKState;
// Handle encoder button press
if (digitalRead(ENCODER_SW_PIN) == LOW) { // Button pressed (active LOW)
if (millis() - lastEncoderBtnPress > DEBOUNCE_DELAY_MS) {
Serial.println("[Encoder] Button Pressed!");
// Example: Toggle system armed state with encoder button
systemArmed = !systemArmed;
Serial.print("[Encoder] System Toggled: ");
Serial.println(systemArmed ? "ARMED" : "DISARMED");
lastEncoderBtnPress = millis();
}
}
}