/*
* IoT Fire Alert System - v2.0
* ESP32-based fire detection with cloud connectivity
*
* Components:
* - ESP32 DevKit V1
* - DHT22 Temperature & Humidity Sensor
* - Flame Sensor (Analog IR)
* - Active Buzzer
* - RGB LED for status indication (3 discrete LEDs)
* - Push Button for manual alert acknowledge
*
* Features (v2.0):
* - Moving-average sensor filtering (data smoothing)
* - Sensor calibration offsets
* - Local data logging to SPIFFS
* - ESP32-native LEDC PWM for LEDs
* - Hardware watchdog timer for fault tolerance
* - ThingSpeak retry with exponential back-off
* - MQTT QoS 1 for reliable alert delivery
* - Buzzer auto-re-enable timer after acknowledge
*
* Cloud Integration: ThingSpeak (HTTP) + MQTT (HiveMQ)
* Simulation: Wokwi.com compatible
*
* Author: IoT Project Team 15
* Date: February 2026
*/
#include <WiFi.h>
#include <HTTPClient.h>
#include <PubSubClient.h>
#include "DHTesp.h"
#include <SPIFFS.h>
#include <esp_task_wdt.h>
// ==================== PIN DEFINITIONS ====================
#define DHT_PIN 15 // DHT22 data pin
#define FLAME_SENSOR_PIN 34 // Flame sensor analog input (ADC1)
#define BUZZER_PIN 25 // Active buzzer
#define LED_RED_PIN 27 // RGB LED - Red
#define LED_GREEN_PIN 26 // RGB LED - Green
#define LED_BLUE_PIN 14 // RGB LED - Blue
#define BUTTON_PIN 13 // Manual reset/silence button
// ==================== LEDC PWM CONFIGURATION ====================
#define LEDC_FREQUENCY 5000 // 5 kHz PWM
#define LEDC_RESOLUTION 8 // 8-bit (0-255)
// ==================== THRESHOLD CONSTANTS ====================
#define TEMP_WARNING_THRESHOLD 40.0 // Temperature warning (°C)
#define TEMP_CRITICAL_THRESHOLD 60.0 // Temperature critical (°C)
#define FLAME_DETECTION_THRESHOLD 500 // Flame sensor threshold (0-4095)
#define HUMIDITY_LOW_THRESHOLD 20.0 // Low humidity warning (%)
// ==================== SENSOR CALIBRATION OFFSETS ====================
// Determined by comparing sensor output against a calibrated reference.
// Adjust these values after calibrating with known temperature/humidity.
#define TEMP_CALIBRATION_OFFSET 0.0 // Add to raw reading (°C)
#define HUMIDITY_CALIBRATION_OFFSET 0.0 // Add to raw reading (%)
#define FLAME_CALIBRATION_OFFSET 0 // Add to raw ADC reading
// ==================== MOVING AVERAGE FILTER ====================
#define FILTER_WINDOW_SIZE 5 // Number of samples in sliding window
// ==================== TIMING CONSTANTS ====================
#define SENSOR_READ_INTERVAL 2000 // Read sensors every 2 seconds
#define CLOUD_UPLOAD_INTERVAL 5000 // Upload to cloud every 5 seconds (faster dashboard sync)
#define ALERT_DEBOUNCE_TIME 3000 // Debounce alerts for 3 seconds
#define BUZZER_PATTERN_INTERVAL 500 // Buzzer pattern timing
#define BUZZER_RE_ENABLE_INTERVAL 30000 // Re-enable buzzer 30s after silence
#define SPIFFS_LOG_INTERVAL 30000 // Log to SPIFFS every 30 seconds
#define THINGSPEAK_RETRY_MAX 3 // Max retry attempts for ThingSpeak
#define WDT_TIMEOUT_SECONDS 10 // Watchdog timeout
// ==================== WIFI CONFIGURATION ====================
const char* WIFI_SSID = "Wokwi-GUEST";
const char* WIFI_PASSWORD = "";
// ==================== THINGSPEAK CONFIGURATION ====================
const char* THINGSPEAK_SERVER = "api.thingspeak.com";
const char* THINGSPEAK_API_KEY = "YOUR_WRITE_API_KEY"; // Replace with your key
const int THINGSPEAK_CHANNEL_ID = 123456; // Replace with your channel
// ==================== MQTT CONFIGURATION ====================
const char* MQTT_BROKER = "broker.hivemq.com";
const int MQTT_PORT = 1883;
const char* MQTT_CLIENT_ID = "ESP32_FireAlert_001";
const char* MQTT_TOPIC_ALERTS = "iot/fire-alert/alerts";
const char* MQTT_TOPIC_SENSORS = "iot/fire-alert/sensors";
const char* MQTT_TOPIC_STATUS = "iot/fire-alert/status";
const char* MQTT_TOPIC_CONTROL = "iot/fire-alert/control";
// ==================== SYSTEM STATE ENUMERATION ====================
enum SystemState {
STATE_NORMAL,
STATE_WARNING,
STATE_CRITICAL,
STATE_ALARM
};
enum AlertType {
ALERT_NONE,
ALERT_HIGH_TEMP,
ALERT_FLAME_DETECTED,
ALERT_LOW_HUMIDITY,
ALERT_MULTI_CONDITION
};
// ==================== MOVING AVERAGE FILTER STRUCT ====================
struct SensorFilter {
float buffer[FILTER_WINDOW_SIZE];
int index;
int count;
float sum;
void init() {
index = 0;
count = 0;
sum = 0.0;
for (int i = 0; i < FILTER_WINDOW_SIZE; i++) buffer[i] = 0.0;
}
float addSample(float value) {
// Subtract oldest value from running sum
sum -= buffer[index];
// Store new value and add to sum
buffer[index] = value;
sum += value;
// Advance circular index
index = (index + 1) % FILTER_WINDOW_SIZE;
if (count < FILTER_WINDOW_SIZE) count++;
// Return the current average
return sum / count;
}
};
// ==================== GLOBAL OBJECTS ====================
DHTesp dht;
WiFiClient wifiClient;
PubSubClient mqttClient(wifiClient);
// Sensor filters (moving average)
SensorFilter tempFilter;
SensorFilter humidityFilter;
SensorFilter flameFilter;
// Raw sensor readings (before filtering)
float rawTemperature = 0.0;
float rawHumidity = 0.0;
int rawFlameValue = 0;
// Filtered / calibrated sensor readings (used for decisions)
float currentTemperature = 0.0;
float currentHumidity = 0.0;
int flameValue = 0;
bool flameDetected = false;
// System state
SystemState currentState = STATE_NORMAL;
AlertType currentAlert = ALERT_NONE;
bool buzzerEnabled = true;
bool alertActive = false;
bool buttonPressed = false;
// Timing variables
unsigned long lastSensorRead = 0;
unsigned long lastCloudUpload = 0;
unsigned long lastAlertTime = 0;
unsigned long lastBuzzerToggle = 0;
unsigned long buzzerSilencedAt = 0; // When buzzer was silenced
unsigned long lastSpiffsLog = 0;
unsigned long lastWiFiReconnect = 0;
bool buzzerState = false;
// Statistics
unsigned long alertCount = 0;
unsigned long systemUptime = 0;
float maxTemperatureRecorded = 0.0;
int cloudUploadSuccessCount = 0;
int cloudUploadFailCount = 0;
int sensorReadErrorCount = 0;
// SPIFFS log state
bool spiffsReady = false;
const char* LOG_FILE = "/fire_log.csv";
// ==================== FUNCTION PROTOTYPES ====================
void setupWiFi();
void setupMQTT();
void setupLEDC();
void setupSPIFFS();
void setupWatchdog();
void readSensors();
void processAlerts();
void updateIndicators();
void uploadToCloud();
void publishMQTT();
void handleMQTTCallback(char* topic, byte* payload, unsigned int length);
void setLEDColor(int red, int green, int blue);
void activateBuzzer();
void deactivateBuzzer();
void logEvent(const char* event);
void logToSPIFFS();
String getSystemStatusJSON();
String getAlertJSON();
String getAlertTypeString(AlertType alert);
String getStateString(SystemState state);
// ==================== SETUP ====================
void setup() {
Serial.begin(115200);
Serial.println("\n========================================");
Serial.println(" IoT Fire Alert System v2.0");
Serial.println(" ESP32-based Fire Detection");
Serial.println("========================================\n");
// Initialize buzzer pin (digital, not PWM)
pinMode(BUZZER_PIN, OUTPUT);
digitalWrite(BUZZER_PIN, LOW);
// Initialize button with internal pull-up
pinMode(BUTTON_PIN, INPUT_PULLUP);
// Initialize LED PWM channels using LEDC
setupLEDC();
setLEDColor(0, 0, 255); // Blue during startup
// Initialize sensor filters
tempFilter.init();
humidityFilter.init();
flameFilter.init();
Serial.println("[INIT] Sensor filters initialized (window=" + String(FILTER_WINDOW_SIZE) + ")");
// Initialize DHT sensor
dht.setup(DHT_PIN, DHTesp::DHT22);
Serial.println("[INIT] DHT22 sensor initialized");
Serial.printf("[INIT] Calibration offsets: Temp=%.1f°C, Humidity=%.1f%%\n",
TEMP_CALIBRATION_OFFSET, HUMIDITY_CALIBRATION_OFFSET);
// Configure ADC for flame sensor
analogReadResolution(12); // 12-bit resolution (0-4095)
analogSetAttenuation(ADC_11db); // Full range 0-3.3V
Serial.println("[INIT] Flame sensor ADC configured (12-bit, 11dB attenuation)");
// Initialize SPIFFS for local data logging
setupSPIFFS();
// Connect to WiFi
setupWiFi();
// Setup MQTT
setupMQTT();
// Enable hardware watchdog timer
setupWatchdog();
// Startup indication
setLEDColor(0, 255, 0); // Green = system ready
Serial.println("\n[READY] Fire Alert System v2.0 initialized successfully");
Serial.println("[INFO] Monitoring started...\n");
// Pre-fill filter with initial readings
delay(2000);
for (int i = 0; i < FILTER_WINDOW_SIZE; i++) {
readSensors();
delay(100);
}
}
// ==================== MAIN LOOP ====================
void loop() {
unsigned long currentMillis = millis();
// Feed the watchdog to prevent reset
esp_task_wdt_reset();
// Reconnect WiFi if it dropped (non-blocking, retry every 10 s)
if (WiFi.status() != WL_CONNECTED) {
if (currentMillis - lastWiFiReconnect >= 10000) {
lastWiFiReconnect = currentMillis;
wl_status_t st = WiFi.status();
// Attempt reconnection (WiFi.begin handles duplicate calls safely)
if (st != WL_IDLE_STATUS) {
Serial.printf("[WIFI] Attempting reconnection (status=%d)...\n", st);
WiFi.disconnect(false); // Disconnect without erasing config
delay(100);
}
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
}
}
// Maintain MQTT connection (only when WiFi is up)
if (WiFi.status() == WL_CONNECTED && !mqttClient.connected()) {
reconnectMQTT();
}
mqttClient.loop();
// Read sensors at defined interval
if (currentMillis - lastSensorRead >= SENSOR_READ_INTERVAL) {
lastSensorRead = currentMillis;
readSensors();
processAlerts();
updateIndicators();
}
// Upload to cloud at defined interval
if (currentMillis - lastCloudUpload >= CLOUD_UPLOAD_INTERVAL) {
lastCloudUpload = currentMillis;
uploadToCloud();
publishMQTT();
}
// Log to SPIFFS at defined interval
if (spiffsReady && (currentMillis - lastSpiffsLog >= SPIFFS_LOG_INTERVAL)) {
lastSpiffsLog = currentMillis;
logToSPIFFS();
}
// Handle button press for manual reset (with debounce state machine)
if (digitalRead(BUTTON_PIN) == LOW) {
if (!buttonPressed) {
buttonPressed = true;
handleButtonPress();
}
} else {
buttonPressed = false;
}
// Update buzzer pattern if alert is active
if (alertActive && buzzerEnabled) {
updateBuzzerPattern(currentMillis);
}
// Auto re-enable buzzer after silence timeout
if (!buzzerEnabled && buzzerSilencedAt > 0) {
unsigned long silenceDuration = currentMillis - buzzerSilencedAt;
if (silenceDuration >= BUZZER_RE_ENABLE_INTERVAL) {
buzzerEnabled = true;
buzzerSilencedAt = 0;
Serial.println("[BUZZER] Auto re-enabled after timeout");
}
}
// Update system uptime
systemUptime = currentMillis / 1000;
}
// ==================== LEDC PWM SETUP (ESP32 Core v3.x API) ====================
void setupLEDC() {
// ledcAttach(pin, freq, resolution) - Core v3.x API
ledcAttach(LED_RED_PIN, LEDC_FREQUENCY, LEDC_RESOLUTION);
ledcAttach(LED_GREEN_PIN, LEDC_FREQUENCY, LEDC_RESOLUTION);
ledcAttach(LED_BLUE_PIN, LEDC_FREQUENCY, LEDC_RESOLUTION);
Serial.println("[INIT] LEDC PWM channels configured for RGB LEDs");
}
// ==================== SPIFFS SETUP ====================
void setupSPIFFS() {
if (SPIFFS.begin(true)) {
spiffsReady = true;
Serial.println("[INIT] SPIFFS mounted successfully");
// Create CSV header if log file does not exist
if (!SPIFFS.exists(LOG_FILE)) {
File f = SPIFFS.open(LOG_FILE, FILE_WRITE);
if (f) {
f.println("uptime_s,temperature_c,humidity_pct,flame_raw,flame_detected,state,alert_type");
f.close();
Serial.println("[SPIFFS] Log file created with header");
}
} else {
// Report existing log size
File f = SPIFFS.open(LOG_FILE, FILE_READ);
if (f) {
Serial.printf("[SPIFFS] Existing log file size: %d bytes\n", f.size());
f.close();
}
}
// Report available space
Serial.printf("[SPIFFS] Total space: %d bytes, Used: %d bytes\n",
SPIFFS.totalBytes(), SPIFFS.usedBytes());
} else {
Serial.println("[ERROR] SPIFFS mount failed - local logging disabled");
}
}
// ==================== WATCHDOG SETUP (ESP-IDF v5.x API) ====================
void setupWatchdog() {
const esp_task_wdt_config_t wdt_config = {
.timeout_ms = WDT_TIMEOUT_SECONDS * 1000,
.idle_core_mask = 0, // Do not watch idle tasks
.trigger_panic = true // Reboot on timeout
};
// Try reconfigure first (TWDT already initialized by Arduino core)
esp_err_t err = esp_task_wdt_reconfigure(&wdt_config);
if (err != ESP_OK) {
Serial.printf("[WARN] WDT reconfigure returned 0x%x, initializing\n", err);
esp_task_wdt_init(&wdt_config);
}
// Add current task to watchdog monitoring
err = esp_task_wdt_add(NULL);
if (err != ESP_OK) {
Serial.printf("[ERROR] Failed to add task to WDT: 0x%x\n", err);
} else {
Serial.printf("[INIT] Watchdog timer configured (%d s timeout)\n", WDT_TIMEOUT_SECONDS);
}
}
// ==================== WIFI SETUP ====================
void setupWiFi() {
Serial.print("[WIFI] Connecting to ");
Serial.println(WIFI_SSID);
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
int attempts = 0;
while (WiFi.status() != WL_CONNECTED && attempts < 30) {
delay(500);
Serial.print(".");
setLEDColor(0, 0, 255); // Blue blink
delay(250);
setLEDColor(0, 0, 0);
delay(250);
attempts++;
}
if (WiFi.status() == WL_CONNECTED) {
Serial.println("\n[WIFI] Connected successfully!");
Serial.print("[WIFI] IP Address: ");
Serial.println(WiFi.localIP());
Serial.print("[WIFI] Signal Strength: ");
Serial.print(WiFi.RSSI());
Serial.println(" dBm");
} else {
Serial.println("\n[WIFI] Connection failed - operating in offline mode");
}
}
// ==================== MQTT SETUP ====================
void setupMQTT() {
mqttClient.setServer(MQTT_BROKER, MQTT_PORT);
mqttClient.setCallback(handleMQTTCallback);
mqttClient.setKeepAlive(60); // 60 second keep-alive for reliable connection
mqttClient.setSocketTimeout(5); // 5 second socket timeout
reconnectMQTT();
}
void reconnectMQTT() {
if (WiFi.status() != WL_CONNECTED) {
return; // Cannot proceed without WiFi
}
if (mqttClient.connected()) {
return; // Already connected
}
int attempts = 0;
while (!mqttClient.connected() && attempts < 3) {
Serial.print("[MQTT] Attempting connection to ");
Serial.print(MQTT_BROKER);
Serial.print("...");
if (mqttClient.connect(MQTT_CLIENT_ID)) {
Serial.println(" connected!");
// Subscribe to control topic with QoS 1 for reliable delivery
mqttClient.subscribe(MQTT_TOPIC_CONTROL, 1);
Serial.print("[MQTT] Subscribed (QoS 1): ");
Serial.println(MQTT_TOPIC_CONTROL);
// Publish online status
String statusMsg = "{\"device\":\"" + String(MQTT_CLIENT_ID) + "\",\"status\":\"online\",\"version\":\"2.0\"}";
mqttClient.publish(MQTT_TOPIC_STATUS, statusMsg.c_str());
return; // Exit on successful connection
} else {
Serial.print(" failed, rc=");
Serial.print(mqttClient.state());
Serial.println(" - retrying in 1s...");
delay(1000);
attempts++;
}
}
if (!mqttClient.connected()) {
Serial.printf("[MQTT] Failed to connect after %d attempts\n", attempts);
}
}
// ==================== SENSOR READING WITH FILTERING & CALIBRATION ====================
void readSensors() {
// ── DHT22 Temperature & Humidity ──
TempAndHumidity data = dht.getTempAndHumidity();
// Validate sensor readings (check for NaN and reasonable ranges)
if (!isnan(data.temperature) && !isnan(data.humidity) &&
data.temperature >= -40.0 && data.temperature <= 80.0 &&
data.humidity >= 0.0 && data.humidity <= 100.0) {
// Apply calibration offsets to raw readings
rawTemperature = data.temperature + TEMP_CALIBRATION_OFFSET;
rawHumidity = data.humidity + HUMIDITY_CALIBRATION_OFFSET;
// Apply moving-average filter for noise reduction
currentTemperature = tempFilter.addSample(rawTemperature);
currentHumidity = humidityFilter.addSample(rawHumidity);
// Track peak temperature
if (currentTemperature > maxTemperatureRecorded) {
maxTemperatureRecorded = currentTemperature;
}
} else {
sensorReadErrorCount++;
Serial.printf("[ERROR] DHT22 read failed (total errors: %d) - Status: %d\n",
sensorReadErrorCount, dht.getStatus());
}
// ── Flame Sensor (Analog) ──
rawFlameValue = analogRead(FLAME_SENSOR_PIN) + FLAME_CALIBRATION_OFFSET;
// Clamp flame value to valid ADC range
if (rawFlameValue < 0) rawFlameValue = 0;
if (rawFlameValue > 4095) rawFlameValue = 4095;
flameValue = (int)flameFilter.addSample((float)rawFlameValue);
flameDetected = (flameValue < FLAME_DETECTION_THRESHOLD);
// ── Log readings to serial ──
Serial.println("─────────────────────────────────────");
Serial.printf("[SENSOR] Temperature: %.1f°C (raw: %.1f°C, filtered)\n",
currentTemperature, rawTemperature);
Serial.printf("[SENSOR] Humidity: %.1f%% (raw: %.1f%%, filtered)\n",
currentHumidity, rawHumidity);
Serial.printf("[SENSOR] Flame: %d (raw: %d, %s)\n",
flameValue, rawFlameValue, flameDetected ? "DETECTED" : "clear");
Serial.printf("[STATE] %s | Alerts: %lu | Uptime: %lus\n",
getStateString(currentState).c_str(), alertCount, systemUptime);
}
// ==================== ALERT PROCESSING (fixed multi-condition logic) ====================
void processAlerts() {
SystemState previousState = currentState;
// Determine the highest-severity individual condition
SystemState highestSeverity = STATE_NORMAL;
AlertType primaryAlert = ALERT_NONE;
int alertConditions = 0;
// Check flame detection (highest priority)
if (flameDetected) {
highestSeverity = STATE_ALARM;
primaryAlert = ALERT_FLAME_DETECTED;
alertConditions++;
logEvent("FLAME DETECTED!");
}
// Check temperature thresholds
if (currentTemperature >= TEMP_CRITICAL_THRESHOLD) {
if (STATE_ALARM > highestSeverity) highestSeverity = STATE_ALARM;
if (primaryAlert == ALERT_NONE) primaryAlert = ALERT_HIGH_TEMP;
alertConditions++;
logEvent("CRITICAL TEMPERATURE!");
} else if (currentTemperature >= TEMP_WARNING_THRESHOLD) {
if (STATE_WARNING > highestSeverity) highestSeverity = STATE_WARNING;
if (primaryAlert == ALERT_NONE) primaryAlert = ALERT_HIGH_TEMP;
alertConditions++;
}
// Check low humidity
if (currentHumidity < HUMIDITY_LOW_THRESHOLD && currentHumidity > 0) {
if (STATE_WARNING > highestSeverity) highestSeverity = STATE_WARNING;
if (primaryAlert == ALERT_NONE) primaryAlert = ALERT_LOW_HUMIDITY;
alertConditions++;
}
// FIX: Multi-condition should ESCALATE, never downgrade
// If any single condition is already ALARM, keep ALARM
if (alertConditions > 1) {
currentAlert = ALERT_MULTI_CONDITION;
// Escalate to at least CRITICAL, but keep ALARM if already there
currentState = (highestSeverity >= STATE_ALARM) ? STATE_ALARM : STATE_CRITICAL;
} else {
currentState = highestSeverity;
currentAlert = primaryAlert;
}
// Handle state transitions
if (currentState >= STATE_WARNING && previousState == STATE_NORMAL) {
alertActive = true;
alertCount++;
lastAlertTime = millis();
Serial.println("\n╔════════════════════════════════════╗");
Serial.println("║ ALERT TRIGGERED ║");
Serial.printf("║ Type: %-28s ║\n", getAlertTypeString(currentAlert).c_str());
Serial.printf("║ Severity: %-24s ║\n", getStateString(currentState).c_str());
Serial.println("╚════════════════════════════════════╝\n");
// Send immediate MQTT alert with QoS 1 for reliable delivery
String alertJSON = getAlertJSON();
mqttClient.publish(MQTT_TOPIC_ALERTS, alertJSON.c_str(), true); // retained
// Force a SPIFFS log entry on alert
if (spiffsReady) logToSPIFFS();
} else if (currentState == STATE_NORMAL && previousState > STATE_NORMAL) {
alertActive = false;
deactivateBuzzer();
Serial.println("[INFO] System returned to NORMAL state");
logEvent("CLEARED - returned to normal");
// Send "all clear" alert so dashboard updates immediately
String clearJSON = "{\"type\":\"ALERT_CLEARED\",\"device\":\"" + String(MQTT_CLIENT_ID) +
"\",\"alertType\":\"NONE\",\"severity\":\"NORMAL\",\"temperature\":" +
String(currentTemperature, 1) + ",\"humidity\":" + String(currentHumidity, 1) +
",\"flameValue\":" + String(flameValue) + ",\"timestamp\":" + String(millis()) + "}";
mqttClient.publish(MQTT_TOPIC_ALERTS, clearJSON.c_str(), true); // retained
Serial.println("[MQTT] ALERT_CLEARED published");
}
// Publish MQTT immediately on ANY state change for real-time dashboard sync
if (currentState != previousState) {
Serial.printf("[STATE] Changed: %s -> %s\n",
getStateString(previousState).c_str(),
getStateString(currentState).c_str());
// Publish with retain flag so dashboard gets latest state on (re)connect
String sensorJSON = getSystemStatusJSON();
mqttClient.publish(MQTT_TOPIC_SENSORS, sensorJSON.c_str(), true); // retained
Serial.println("[MQTT] State change published (retained)");
}
}
// ==================== INDICATOR UPDATES ====================
void updateIndicators() {
switch (currentState) {
case STATE_NORMAL:
setLEDColor(0, 255, 0); // Green
deactivateBuzzer();
break;
case STATE_WARNING:
setLEDColor(255, 165, 0); // Orange
break;
case STATE_CRITICAL:
setLEDColor(255, 100, 0); // Orange-Red
if (buzzerEnabled) activateBuzzer();
break;
case STATE_ALARM:
setLEDColor(255, 0, 0); // Red
if (buzzerEnabled) activateBuzzer();
break;
}
}
void updateBuzzerPattern(unsigned long currentMillis) {
int interval;
switch (currentState) {
case STATE_WARNING:
interval = 1000; // Slow beep for warning
break;
case STATE_CRITICAL:
interval = 500; // Medium beep for critical
break;
case STATE_ALARM:
interval = 200; // Rapid beep for alarm
break;
default:
interval = 0;
}
if (interval > 0 && (currentMillis - lastBuzzerToggle >= (unsigned long)interval)) {
lastBuzzerToggle = currentMillis;
buzzerState = !buzzerState;
digitalWrite(BUZZER_PIN, buzzerState ? HIGH : LOW);
}
}
// ==================== CLOUD UPLOAD WITH RETRY ====================
void uploadToCloud() {
if (WiFi.status() != WL_CONNECTED) {
Serial.println("[CLOUD] WiFi not connected - skipping upload");
return;
}
HTTPClient http;
String url = "http://" + String(THINGSPEAK_SERVER) + "/update?api_key=" +
String(THINGSPEAK_API_KEY) +
"&field1=" + String(currentTemperature, 1) +
"&field2=" + String(currentHumidity, 1) +
"&field3=" + String(flameValue) +
"&field4=" + String(flameDetected ? 1 : 0) +
"&field5=" + String(currentState) +
"&field6=" + String(alertCount) +
"&field7=" + String(WiFi.RSSI()) +
"&field8=" + String(systemUptime);
// Retry with exponential back-off (WDT-safe: feed watchdog during waits)
bool success = false;
for (int attempt = 1; attempt <= THINGSPEAK_RETRY_MAX; attempt++) {
http.begin(url);
http.setTimeout(3000); // 3 second timeout (reduced to stay within WDT window)
int httpResponseCode = http.GET();
http.end();
if (httpResponseCode == 200) {
cloudUploadSuccessCount++;
Serial.printf("[CLOUD] ThingSpeak upload OK (attempt %d, total success: %d)\n",
attempt, cloudUploadSuccessCount);
success = true;
break;
} else {
Serial.printf("[CLOUD] ThingSpeak attempt %d/%d failed (HTTP %d)\n",
attempt, THINGSPEAK_RETRY_MAX, httpResponseCode);
if (attempt < THINGSPEAK_RETRY_MAX) {
unsigned long backoff = 500 * attempt; // 500ms, 1000ms, 1500ms
Serial.printf("[CLOUD] Retrying in %lu ms...\n", backoff);
// Feed watchdog during backoff to prevent WDT reset
unsigned long waitStart = millis();
while (millis() - waitStart < backoff) {
esp_task_wdt_reset();
delay(100);
}
}
}
}
if (!success) {
cloudUploadFailCount++;
Serial.printf("[CLOUD] Upload FAILED after %d attempts (total failures: %d)\n",
THINGSPEAK_RETRY_MAX, cloudUploadFailCount);
}
}
void publishMQTT() {
if (!mqttClient.connected()) return;
String sensorJSON = getSystemStatusJSON();
mqttClient.publish(MQTT_TOPIC_SENSORS, sensorJSON.c_str());
Serial.println("[MQTT] Sensor data published");
}
// ==================== MQTT CALLBACK ====================
void handleMQTTCallback(char* topic, byte* payload, unsigned int length) {
String message = "";
for (unsigned int i = 0; i < length; i++) {
message += (char)payload[i];
}
Serial.printf("[MQTT] Received on %s: %s\n", topic, message.c_str());
if (String(topic) == MQTT_TOPIC_CONTROL) {
if (message == "SILENCE") {
buzzerEnabled = false;
buzzerSilencedAt = millis();
deactivateBuzzer();
Serial.println("[CONTROL] Buzzer silenced (auto re-enable in 30s)");
} else if (message == "ENABLE_BUZZER") {
buzzerEnabled = true;
buzzerSilencedAt = 0;
Serial.println("[CONTROL] Buzzer enabled");
} else if (message == "RESET") {
handleButtonPress();
} else if (message == "STATUS") {
String status = getSystemStatusJSON();
mqttClient.publish(MQTT_TOPIC_STATUS, status.c_str());
}
}
// Note: PubSubClient v2+ automatically handles ACK for QoS 1/2 messages
}
// ==================== BUTTON HANDLER ====================
void handleButtonPress() {
Serial.println("[INPUT] Button pressed - acknowledging alert");
if (alertActive) {
buzzerEnabled = false;
buzzerSilencedAt = millis(); // Start re-enable countdown
deactivateBuzzer();
Serial.printf("[INFO] Alert acknowledged - buzzer silenced (re-enables in %ds)\n",
BUZZER_RE_ENABLE_INTERVAL / 1000);
}
}
// ==================== LED CONTROL (LEDC PWM — ESP32 Core v3.x) ====================
void setLEDColor(int red, int green, int blue) {
// Clamp values to valid 8-bit range
red = constrain(red, 0, 255);
green = constrain(green, 0, 255);
blue = constrain(blue, 0, 255);
// Write PWM values using pin (Core v3.x API)
ledcWrite(LED_RED_PIN, red);
ledcWrite(LED_GREEN_PIN, green);
ledcWrite(LED_BLUE_PIN, blue);
}
// ==================== BUZZER CONTROL ====================
void activateBuzzer() {
if (!buzzerState) {
buzzerState = true;
digitalWrite(BUZZER_PIN, HIGH);
}
}
void deactivateBuzzer() {
buzzerState = false;
digitalWrite(BUZZER_PIN, LOW);
}
// ==================== LOCAL DATA LOGGING (SPIFFS) ====================
void logToSPIFFS() {
if (!spiffsReady) return;
// Rotate log file if it exceeds ~100 KB to prevent filling flash
File checkFile = SPIFFS.open(LOG_FILE, FILE_READ);
if (checkFile) {
if (checkFile.size() > 100000) {
checkFile.close();
SPIFFS.remove("/fire_log_old.csv");
SPIFFS.rename(LOG_FILE, "/fire_log_old.csv");
File newFile = SPIFFS.open(LOG_FILE, FILE_WRITE);
if (newFile) {
newFile.println("uptime_s,temperature_c,humidity_pct,flame_raw,flame_detected,state,alert_type");
newFile.close();
}
Serial.println("[SPIFFS] Log rotated (old data archived)");
} else {
checkFile.close();
}
}
// Append new data row
File f = SPIFFS.open(LOG_FILE, FILE_APPEND);
if (f) {
f.printf("%lu,%.1f,%.1f,%d,%d,%s,%s\n",
systemUptime,
currentTemperature,
currentHumidity,
flameValue,
flameDetected ? 1 : 0,
getStateString(currentState).c_str(),
getAlertTypeString(currentAlert).c_str());
f.close();
Serial.println("[SPIFFS] Data logged to flash");
} else {
Serial.println("[ERROR] Failed to write to SPIFFS log");
}
}
// ==================== UTILITY FUNCTIONS ====================
void logEvent(const char* event) {
Serial.printf("[EVENT] %lu s - %s\n", millis() / 1000, event);
}
String getSystemStatusJSON() {
String json = "{";
json += "\"device\":\"" + String(MQTT_CLIENT_ID) + "\",";
json += "\"version\":\"2.0\",";
json += "\"temperature\":" + String(currentTemperature, 1) + ",";
json += "\"humidity\":" + String(currentHumidity, 1) + ",";
json += "\"rawTemperature\":" + String(rawTemperature, 1) + ",";
json += "\"rawHumidity\":" + String(rawHumidity, 1) + ",";
json += "\"flameValue\":" + String(flameValue) + ",";
json += "\"flameDetected\":" + String(flameDetected ? "true" : "false") + ",";
json += "\"state\":\"" + getStateString(currentState) + "\",";
json += "\"alertType\":\"" + getAlertTypeString(currentAlert) + "\",";
json += "\"alertCount\":" + String(alertCount) + ",";
json += "\"uptime\":" + String(systemUptime) + ",";
json += "\"maxTemp\":" + String(maxTemperatureRecorded, 1) + ",";
json += "\"wifiRSSI\":" + String(WiFi.RSSI()) + ",";
json += "\"buzzerEnabled\":" + String(buzzerEnabled ? "true" : "false") + ",";
json += "\"cloudSuccesses\":" + String(cloudUploadSuccessCount) + ",";
json += "\"cloudFailures\":" + String(cloudUploadFailCount) + ",";
json += "\"sensorErrors\":" + String(sensorReadErrorCount);
json += "}";
return json;
}
String getAlertJSON() {
String json = "{";
json += "\"type\":\"FIRE_ALERT\",";
json += "\"device\":\"" + String(MQTT_CLIENT_ID) + "\",";
json += "\"alertType\":\"" + getAlertTypeString(currentAlert) + "\",";
json += "\"severity\":\"" + getStateString(currentState) + "\",";
json += "\"temperature\":" + String(currentTemperature, 1) + ",";
json += "\"humidity\":" + String(currentHumidity, 1) + ",";
json += "\"flameValue\":" + String(flameValue) + ",";
json += "\"timestamp\":" + String(millis());
json += "}";
return json;
}
String getAlertTypeString(AlertType alert) {
switch (alert) {
case ALERT_NONE: return "NONE";
case ALERT_HIGH_TEMP: return "HIGH_TEMPERATURE";
case ALERT_FLAME_DETECTED: return "FLAME_DETECTED";
case ALERT_LOW_HUMIDITY: return "LOW_HUMIDITY";
case ALERT_MULTI_CONDITION: return "MULTIPLE_CONDITIONS";
default: return "UNKNOWN";
}
}
String getStateString(SystemState state) {
switch (state) {
case STATE_NORMAL: return "NORMAL";
case STATE_WARNING: return "WARNING";
case STATE_CRITICAL: return "CRITICAL";
case STATE_ALARM: return "ALARM";
default: return "UNKNOWN";
}
}
IoT Fire Alert System
Temp/Humidity
Flame Sensor
Status