#include #include "RTClib.h" #include #include #include #include #include "FS.h" #include #include // Include the vector library // --- MQ2 Sensor & RTC Configuration --- #define MQ2pin 2 // analog pin for MQ2 sensor (can be 2 or 26) #define Buzzer 5 // buzzer pin #define RL_VAL 10.0 // load resistor in kΩ //#define Ro 3.3 // default Ro in clean air (kΩ), approximated #define Ro 288.73 // Change your global Ro definition //float Ro_calibrated = 0; // Initialize global Ro int dangerPPM = 2000; // 2000 ppm threshold for LPG alert RTC_DS3231 rtc; // RTC object // Removed daysOfTheWeek array as it's no longer needed for the timestamp string. // char daysOfTheWeek[7][12] = {"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"}; // --- Network & Firebase Credentials --- #define WIFI_SSID "TP-Link_1B9D" #define WIFI_PASSWORD "91884084" // #define WIFI_SSID "LyonsNET" // #define WIFI_PASSWORD "golyons!" // #define WIFI_SSID "AAM_Eero" // #define WIFI_PASSWORD "86ParkStr33t" #define Web_API_KEY "AIzaSyDE3BncTtJQurEM3Zb-W99pMVIOlV2HOLI" #define DATABASE_URL "https://data-check-ccce9-default-rtdb.firebaseio.com/" #define USER_EMAIL "ondrasek_hanna@wheatoncollege.edu" #define USER_PASS "Welcome1*" // --- LittleFS Configuration --- #define FORMAT_LITTLEFS_IF_FAILED true // Set to true to format LittleFS on first boot or if corrupted // --- Global Variables --- // Firebase Components UserAuth user_auth(Web_API_KEY, USER_EMAIL, USER_PASS); FirebaseApp app; WiFiClientSecure ssl_client; using AsyncClient = AsyncClientClass; AsyncClient aClient(ssl_client); RealtimeDatabase Database; // Data Structure for Miner Entries struct MinerDataEntry { String entryNum; String dangerStatus; float gasPPM; String minerId; String timestamp; String country; }; // Global vector to store new data entries read from LittleFS std::vector newEntriesToProcess; // Timer variables for periodic tasks (Firebase push) unsigned long lastSendTime = 0; const unsigned long sendInterval = 10000; // 10 seconds in milliseconds (for Firebase push) unsigned long lastSensorReadTime = 0; const unsigned long sensorReadInterval = 1000; // 1 second in milliseconds (for sensor reading and local logging) static unsigned long entryCounter = 0; // Persistent counter for entries // --- Function Prototypes --- void processFirebaseResult(AsyncResult &aResult); void writeFile(fs::FS &fs, const char *path, const char *message); void appendFile(fs::FS &fs, const char *path, const char *message); void readAndProcessNewEntriesFromFile(fs::FS &fs, const char *path); void pushEntriesToFirebase(); float readRs(int analogPin, float loadResistor); float estimateLPGppm(float rs, float ro); // --- MQ2 Sensor Functions --- float readRs(int analogPin, float loadResistor) { int adc = analogRead(analogPin); float voltage = adc * (3.3 / 4095.0); // Corrected for 12-bit ADC 0-4095 float rs = (3.3 - voltage) * loadResistor / voltage; return rs; } float estimateLPGppm(float rs, float ro) { float rs_ro_ratio = rs / ro; float m = -0.50; // LPG curve approximation from datasheet float b = 1.10; float logRatio = log10(rs_ro_ratio); float logPPM = (logRatio - b) / m; float ppm = pow(10, logPPM); return ppm; } // --- LittleFS Helper Functions --- void writeFile(fs::FS &fs, const char *path, const char *message) { File file = fs.open(path, FILE_WRITE); if (!file) { Serial.println("- Failed to open file for writing"); return; } if (file.print(message)) { Serial.println("- File written successfully."); } else { Serial.println("- File write failed."); } file.close(); } void appendFile(fs::FS &fs, const char *path, const char *message) { File file = fs.open(path, FILE_APPEND); if (!file) { Serial.println("- Failed to open file for appending"); return; } if (file.print(message)) { Serial.println("- Message appended successfully."); } else { Serial.println("- Append failed."); } file.close(); } int readLastProcessedLine(fs::FS &fs, const char *path = "/last_read.txt") { File file = fs.open(path, FILE_READ); if (!file) return 0; String numStr = file.readStringUntil('\n'); file.close(); return numStr.toInt(); // Defaults to 0 if unreadable } void writeLastProcessedLine(fs::FS &fs, int lineNumber, const char *path = "/last_read.txt") { File file = fs.open(path, FILE_WRITE); if (!file) return; file.printf("%d\n", lineNumber); file.close(); } void readAndProcessNewEntriesFromFile(fs::FS &fs, const char *path) { Serial.println("\n--- Starting file read for new entries ---"); File file = fs.open(path, FILE_READ); if (!file || file.isDirectory()) { Serial.println("- Failed to open file for reading."); return; } int lastProcessedLine = readLastProcessedLine(fs); int currentLineNum = 0; String currentLine = ""; newEntriesToProcess.clear(); Serial.printf("- Skipping to line %d (last processed)\n", lastProcessedLine); while (file.available()) { char c = file.read(); currentLine += c; if (c == '\n') { currentLineNum++; if (currentLineNum <= lastProcessedLine) { currentLine = ""; continue; } MinerDataEntry entry; int lastCommaIndex = -1; int fieldCount = 0; for (int i = 0; i < currentLine.length(); i++) { char fieldChar = currentLine.charAt(i); if (fieldChar == ',' || fieldChar == '\n') { String field = currentLine.substring(lastCommaIndex + 1, i); field.trim(); switch (fieldCount) { case 0: entry.entryNum = field; break; case 1: entry.dangerStatus = field; break; case 2: entry.gasPPM = field.toFloat(); break; case 3: entry.minerId = field; break; case 4: entry.timestamp = field; break; // This now correctly maps to the timestamp case 5: entry.country = field; entry.country.replace("\r", ""); break; } fieldCount++; lastCommaIndex = i; } } if (fieldCount >= 6) { newEntriesToProcess.push_back(entry); Serial.printf(" --> Queued new entry %s\n", entry.entryNum.c_str()); } else { Serial.printf(" Malformed entry on line %d (skipped): %s\n", currentLineNum, currentLine.c_str()); } currentLine = ""; // reset for next } } file.close(); Serial.printf("- Finished. %d new entries queued.\n", newEntriesToProcess.size()); if (currentLineNum > lastProcessedLine) { writeLastProcessedLine(fs, currentLineNum); Serial.printf("- Updated last processed line to %d\n", currentLineNum); } } // --- Firebase Push Function --- void pushEntriesToFirebase() { if (WiFi.status() != WL_CONNECTED) { Serial.println("Wi-Fi not connected. Skipping Firebase push."); return; } if (!newEntriesToProcess.empty()) { Serial.println("\n--- Pushing New Entries to Firebase Realtime Database ---"); for (const auto& entry : newEntriesToProcess) { String path = "/readings/" + entry.entryNum; Database.set(aClient, path + "/danger", entry.dangerStatus, processFirebaseResult, "RTDB_danger_" + entry.entryNum); Database.set(aClient, path + "/gas_ppm", entry.gasPPM, processFirebaseResult, "RTDB_gas_ppm_" + entry.entryNum); Database.set(aClient, path + "/miner_id", entry.minerId, processFirebaseResult, "RTDB_miner_id_" + entry.entryNum); Database.set(aClient, path + "/timestamp", entry.timestamp, processFirebaseResult, "RTDB_timestamp_" + entry.entryNum); // Corrected mapping Database.set(aClient, path + "/country", entry.country, processFirebaseResult, "RTDB_country_" + entry.entryNum); // Corrected mapping Serial.printf(" Sent Entry %s to Firebase.\n", entry.entryNum.c_str()); } newEntriesToProcess.clear(); Serial.println("- All queued entries sent to Firebase and cleared from local vector."); } else { Serial.println("\n--- No new entries to push to Firebase this cycle. ---"); } } // --- Firebase Result Processing Function --- void processFirebaseResult(AsyncResult &aResult) { if (!aResult.isResult()) return; if (aResult.isEvent()) Serial.printf("Event task: %s, msg: %s, code: %d\n", aResult.uid().c_str(), aResult.eventLog().message().c_str(), aResult.eventLog().code()); if (aResult.isDebug()) Serial.printf("Debug task: %s, msg: %s\n", aResult.uid().c_str(), aResult.debug().c_str()); if (aResult.isError()) Serial.printf("Error task: %s, msg: %s, code: %d\n", aResult.uid().c_str(), aResult.error().message().c_str(), aResult.error().code()); if (aResult.available()) Serial.printf("task: %s, payload: %s\n", aResult.uid().c_str(), aResult.c_str()); } // --- Setup Function --- void setup() { Serial.begin(115200); pinMode(Buzzer, OUTPUT); Serial.println("\n--- Starting ESP32 Setup ---"); // MQ2 Sensor warm-up Serial.println("MQ2 warming up!"); delay(20000); // Allow the MQ2 to warm up // Start RTC if (!rtc.begin()) { Serial.println("Couldn't find RTC"); while (1) delay(10); } if (rtc.lostPower()) { Serial.println("RTC lost power, setting time to compile time"); rtc.adjust(DateTime(F(__DATE__), F(__TIME__))); } Serial.println("RTC Initialized."); // Connect to Wi-Fi Serial.printf("Connecting to Wi-Fi: %s", WIFI_SSID); WiFi.begin(WIFI_SSID, WIFI_PASSWORD); while (WiFi.status() != WL_CONNECTED) { Serial.print("."); delay(300); } Serial.println("\nWi-Fi Connected!"); Serial.printf("IP Address: %s\n", WiFi.localIP().toString().c_str()); // Configure SSL client for Firebase ssl_client.setInsecure(); // This disables certificate validation, use with caution in production ssl_client.setConnectionTimeout(1000); ssl_client.setHandshakeTimeout(5); // Initialize Firebase Async Client and App initializeApp(aClient, app, getAuth(user_auth), processFirebaseResult, "🔐 authTask"); app.getApp(Database); Database.url(DATABASE_URL); Serial.println("Firebase Initialized."); // Initialize LittleFS if (!LittleFS.begin(FORMAT_LITTLEFS_IF_FAILED)) { Serial.println("LittleFS Mount Failed!"); return; } else { Serial.println("LittleFS Mounted Successfully."); } } // --- Loop Function --- void loop() { app.loop(); // Maintain Firebase authentication and async tasks // Check if Wi-Fi is connected and Firebase is ready if (WiFi.status() == WL_CONNECTED && app.ready()) { unsigned long currentTime = millis(); // --- Gas Sensor Reading and Local Logging (every 1 second) --- if (currentTime - lastSensorReadTime >= sensorReadInterval) { lastSensorReadTime = currentTime; // Get sensor reading and convert to PPM float rs = readRs(MQ2pin, RL_VAL); float ppm = estimateLPGppm(rs, Ro); // Get RTC time DateTime now = rtc.now(); String yearStr = String(now.year(), DEC); String monthStr = (now.month() < 10 ? "0" : "") + String(now.month(), DEC); String dayStr = (now.day() < 10 ? "0" : "") + String(now.day(), DEC); String hourStr = (now.hour() < 10 ? "0" : "") + String(now.hour(), DEC); String minuteStr = (now.minute() < 10 ? "0" : "") + String(now.minute(), DEC); String secondStr = (now.second() < 10 ? "0" : "") + String(now.second(), DEC); // Corrected timestamp format: YYYY-MM-DD HH:MM:SS String timestamp = yearStr + "-" + monthStr + "-" + dayStr + " " + hourStr + ":" + minuteStr + ":" + secondStr; // Determine danger status and activate buzzer if necessary String danger_status = (ppm >= dangerPPM) ? "DANGER" : "SAFE"; if (ppm >= dangerPPM) { Serial.println("Danger: Gas concentration exceeded safe limit"); digitalWrite(Buzzer, HIGH); // buzzer on } else { digitalWrite(Buzzer, LOW); // buzzer off } // Print readings to serial monitor Serial.print("Timestamp: "); Serial.println(timestamp); Serial.print("Sensor Rs: "); Serial.print(rs); Serial.print(" kΩ, Estimated LPG PPM: "); Serial.println(ppm); Serial.print("Danger Status: "); Serial.println(danger_status); // Prepare data for logging entryCounter++; String entry_num = "entry" + String(entryCounter); String miner_id = "miner_ESP32C3"; // Placeholder String country_name = "USA"; // Corrected country string // Corrected order for logging to LittleFS String dataToLog = entry_num + "," + danger_status + "," + String(ppm) + "," + miner_id + "," + timestamp + "," + country_name + "\n"; Serial.println("\n--- Appending New Data Entry to LittleFS ---"); appendFile(LittleFS, "/data.csv", dataToLog.c_str()); // Read newly appended data from LittleFS and queue for Firebase readAndProcessNewEntriesFromFile(LittleFS, "/data.csv"); } // --- Periodic Firebase Push (every 10 seconds) --- if (currentTime - lastSendTime >= sendInterval) { lastSendTime = currentTime; // Update the last send time pushEntriesToFirebase(); // Call the function to push queued entries } } else if (WiFi.status() != WL_CONNECTED) { Serial.println("Wi-Fi is disconnected. Attempting to reconnect..."); WiFi.reconnect(); delay(1000); // Small delay before next reconnection attempt } else { Serial.println("Firebase not ready. Waiting for authentication."); delay(1000); // Wait for Firebase to become ready } }