From this website's homepage:
How does this project accomplish this? The microcontroller, a XIAO ESP32C3, is connected to an MQ2 gas sensor module, a loud active buzzer, and a real-time clock module. I chose the XIAO for its Wifi capabilities and small footprint. While the miner is underground, entries (consisting of a read from the real-time clock module and the mcq2 sensor) are added to a csv file on the microcontroller's flash memory. If the gas ppm goes above 2000 ppm (I'll discuss why this specific number later in the webpage), the miner is alerted using the loud active buzzer regardless of whether or not wifi is connected. When the miner comes above ground, and if there is a Wifi network in the vicinity, or if the device is taken to somewhere with Wifi, the CSV entries are pushed to the Firebase Real-Time Database and the dashboard updates. However, let's delve into some specifics.
First, I really tried to understand the basic code for pushing data to firebase through an ESP32 and any quirks associated with the XIAO and connecting to Wifi. The XIAO ESP32C3 definitely has strong Wifi capabilities. However, I did notice a few things. First, the XIAO obviously has an easier time connecting to Wifi when the antenna module is connected, so it is definitely worth sacrificing some space in a circuit board to attach it.
Second, the firebase library takes a very long time to compile. It might be worth setting up PlatformIO for any project involving the firebase library.
Finally, it matters what Wifi network you connect the XIAO to. Any 5GHz network is unacceptable for connecting a XIAO to. Most microcontrollers in the ESP32 family are designed to operate within the 2.4GHz frequency band, not in the 5GHz frequency. Also, in my experience, phone hotspot Wifi will not work with the XIAO. Surprisingly, Wifi designated for devices such as TVs is connectable for the XIAO. For example, at my college, we have a Wifi network called LyonsNet that is designated for things like Roku TVs and other devices. This was the only connectable network at my college. Our guest Wifi was not connectable, and neither was our secure college Wifi network. Both of those require authentication. Any Wifi requiring authentication is clearly not acceptable for XIAO connection.
/*********
Rui Santos & Sara Santos - Random Nerd Tutorials
Complete instructions at https://RandomNerdTutorials.com/esp32-firebase-realtime-database/
*********/
#include <Arduino.h>
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <FirebaseClient.h>
// Network and Firebase credentials
#define WIFI_SSID "insert yours here"
#define WIFI_PASSWORD "insert yours here"
#define Web_API_KEY "insert yours here"
#define DATABASE_URL "insert yours here"
#define USER_EMAIL "insert yours here"
#define USER_PASS "insert yours here"
// User function
void processData(AsyncResult &aResult);
// Authentication
UserAuth user_auth(Web_API_KEY, USER_EMAIL, USER_PASS);
// Firebase components
FirebaseApp app;
WiFiClientSecure ssl_client;
using AsyncClient = AsyncClientClass;
AsyncClient aClient(ssl_client);
RealtimeDatabase Database;
// Timer variables for sending data every 10 seconds
unsigned long lastSendTime = 0;
const unsigned long sendInterval = 10000; // 10 seconds in milliseconds
// Variables to send to the database
String danger = "true";
int intValue = 350;
String miner_id = "hanna_miner_may7";
String timestamp = "2025-05-07T19:31:00Z";
void setup(){
Serial.println("i got to beginning of setup");
Serial.begin(9600);
// Connect to Wi-Fi
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
Serial.print("Connecting to Wi-Fi");
while (WiFi.status() != WL_CONNECTED) {
Serial.print(".");
delay(300);
}
Serial.println();
// Configure SSL client
ssl_client.setInsecure();
ssl_client.setConnectionTimeout(1000);
ssl_client.setHandshakeTimeout(5);
// Initialize Firebase
initializeApp(aClient, app, getAuth(user_auth), processData, "🔐 authTask");
app.getApp(Database);
Database.url(DATABASE_URL);
Serial.println("i got to end of setup");
}
void loop(){
// Maintain authentication and async tasks
app.loop();
// Check if authentication is ready
if (app.ready()){
// Periodic data sending every 10 seconds
unsigned long currentTime = millis();
if (currentTime - lastSendTime >= sendInterval){
// Update the last send time
lastSendTime = currentTime;
// send a string -- DONE
Database.set(aClient, "/readings/entry5/danger", danger, processData, "RTDB_Send_String");
Serial.println("i got into the loop after sending 1st string");
// send an int -- DONE
Database.set(aClient, "/readings/entry5/gas_ppm", intValue, processData, "RTDB_Send_Int");
// send a string -- DONE
Database.set(aClient, "/readings/entry5/miner_id", miner_id, processData, "RTDB_Send_String");
// send a string -- DONE
Database.set(aClient, "/readings/entry5/timestamp", timestamp, processData, "RTDB_Send_String");
}
}
}
void processData(AsyncResult &aResult) {
if (!aResult.isResult())
return;
if (aResult.isEvent())
Firebase.printf("Event task: %s, msg: %s, code: %d\n", aResult.uid().c_str(), aResult.eventLog().message().c_str(), aResult.eventLog().code());
if (aResult.isDebug())
Firebase.printf("Debug task: %s, msg: %s\n", aResult.uid().c_str(), aResult.debug().c_str());
if (aResult.isError())
Firebase.printf("Error task: %s, msg: %s, code: %d\n", aResult.uid().c_str(), aResult.error().message().c_str(), aResult.error().code());
if (aResult.available())
Firebase.printf("task: %s, payload: %s\n", aResult.uid().c_str(), aResult.c_str());
}
Before I moved on, I wanted to make sure that the smallest possible instance of firebase pushes would work. At this point, I just used fixed variables that looked like what we would later expect real entries to look like.
My code connecting to wifi and debugging each task, as well as sending each task (each of the items in the entry). This output in the serial monitor indicates a successful push to firebase.
Jumping ahead a bit, but this is proof that the entries show in the dashboard.
After I got the pushes with dummy variables to work, I really wanted to make a clean real-time dashboard to really cement the idea of ease of viewing the long-term effects of gas exposure. You must set up web app authentication through your firebase project. Otherwise, this is basically HTML & CSS, without a connection to the firebase database.
Gas Exposure Dashboard
Miner Gas Exposure Dashboard
Miner ID
Timestamp
Gas (ppm)
Danger
Entries are not accurate to above (above is real-time), but this is how the UI looks.
body {
font-family: Arial, sans-serif;
padding: 30px;
background-color: #f9f9f9;
color: #333;
}
h2 {
text-align: center;
margin-bottom: 30px;
}
table {
width: 100%;
border-collapse: collapse;
margin: auto;
background-color: white;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
th, td {
padding: 15px;
text-align: center;
border-bottom: 1px solid #eee;
}
th {
background-color: #0077cc;
color: white;
text-transform: uppercase;
font-size: 14px;
}
tr:hover {
background-color: #f1f1f1;
}
.danger-yes {
color: white;
background-color: #e74c3c;
font-weight: bold;
padding: 5px 10px;
border-radius: 4px;
}
.danger-no {
color: white;
background-color: #2ecc71;
font-weight: bold;
padding: 5px 10px;
border-radius: 4px;
}
Next, I wanted to test the sensor code independently before integrating with LittleFS and Firebase. This involved setting up the RTC code, setting up the equation for getting the accurate ppm from the basic MQ2 sensor, and making the buzzer sound when the ppm reached above the danger threshold. The RTC module is relatively easy, it serves as a dedicated, battery-backed clock that accurately tracks the current date and time independently of the microcontroller's power state with a coin battery. The code uses the Wire library for its I2C communication protocol and the RTClib library to simplify time retrieval and setting operations. Upon initialization, the system verifies the RTC, and if it detects a loss of power, it automatically synchronizes the RTC's time with the project's compilation timestamp. This continuous timekeeping functionality allows the system to accurately timestamp sensor data, providing context for gas ppm levels. The buzzer is relatively simple, buzzing for 20 seconds if dangerPPM or greater is reached. The code for the MQ2 is a little more complicated.
The MQ2 sensor in this code operates by measuring changes in its electrical resistance when exposed to gases. This resistance reading is then processed using the relationship derived from the sensitivity characteristics graph, specifically the liquified petroleum gas (LPG) curve. LPGs are dangerous, they include butane and propane, which are explosive and can cause asphyxiation. This graph allows for the conversion of the sensor's resistance ratio (rs/ro) into an estimated gas concentration in ppm. The LPG curve was intentionally chosen for the alert threshold because its position as the lowest line on the graph means that the sensor is highly sensitive to LPG, allowing the system to detect this and similar combustible gases at lower concentrations and thus provide an earlier warning by activating the buzzer. The dangerppm variable is set to 2,000 ppm, because starting at 2,000 ppm, LPGs are considered immediately dangerous to life or health by the CDC
#include <Wire.h>
#include "RTClib.h"
#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
int dangerPPM = 2000; // 2000 ppm threshold for LPG alert
RTC_DS3231 rtc;
char daysOfTheWeek[7][12] = {"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"};
// function to read Rs using analog voltage
float readRs(int analogPin, float loadResistor) {
int adc = analogRead(analogPin);
// CHANGE THIS LINE:
float voltage = adc * (3.3 / 4095.0); // Corrected for 12-bit ADC (0-4095)
float rs = (3.3 - voltage) * loadResistor / voltage;
return rs;
}
// function to estimate LPG PPM based on Rs/Ro ratio
float estimateLPGppm(float rs, float ro) {
float rs_ro_ratio = rs / ro;
// LPG curve approximation from datasheet
float m = -0.38;
float b = 1.15;
float logRatio = log10(rs_ro_ratio);
float logPPM = (logRatio - b) / m;
float ppm = pow(10, logPPM);
return ppm;
}
void setup() {
Serial.begin(9600);
pinMode(Buzzer, OUTPUT);
Serial.println("MQ2 warming up!");
// allow the MQ2 to warm up
delay(20000);
// 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__)));
}
}
void loop() {
// 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);
String dayOfWeek = daysOfTheWeek[now.dayOfTheWeek()];
String timestamp = dayOfWeek + ", " + yearStr + "-" + monthStr + "-" + dayStr + " " + hourStr + ":" + minuteStr + ":" + secondStr;
// print readings
Serial.print("Timestamp: ");
Serial.println(timestamp);
Serial.print("Sensor Rs: ");
Serial.print(rs);
Serial.print(" kΩ, Estimated LPG PPM: ");
Serial.println(ppm);
// if ppm exceeds threshold, activate buzzer
if (ppm >= dangerPPM) {
Serial.println("Danger: Gas concentration exceeded safe limit");
digitalWrite(Buzzer, HIGH); // buzzer on
delay(20000); // give the miner time to react
digitalWrite(Buzzer, LOW); // buzzer off
}
delay(15000); // sample every 15 seconds
}
First, I want to start off with the warning about SPIFFS. It looks much easier to use than LittleFS based on the tutorials. SPIFFS has a very lightweight design and minimal memory overhead, which can make it seem easier for basic, static file storage without needing to consider more complex features like directory support or power-loss protection, which are inherent to LittleFS's design. However, DON'T USE SPIFFS!!!!!! SPIFFS is not supported by any Arduino version past version 1.8.x. It's considered depreciated past version 2.x. This in tandem with the firebase library compile time made it just about a nightmare. LittleFS is resilient to power loss and is non-depreciated.
The code starts by connecting to a Wi-Fi network and then preparing to send data to firebase. Simultaneously, it sets up LittleFS, a local file system on the XIAO itself, to create or update a file named /data.csv. This file serves as a crucial temporary storage, ensuring that no sensor readings are lost, even if the internet connection becomes unavailable. In its continuous operation, the XIAO simulates new "miner" data—including gas levels, danger status, miner ID, timestamp, and country. It saves this information to the /data.csv file. A "stopread" marker system is used to track which data has already been processed, ensuring that only the latest readings are considered. Periodically, when the Wi-Fi connection is stable, the ESP32 reads these new entries from its local storage and transmits them to the cloud database.
/*********
Rui Santos & Sara Santos - Random Nerd Tutorials (adapted)
Complete instructions at https://RandomNerdTutorials.com/esp32-firebase-realtime-database/
*********/
#include <Arduino.h>
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <FirebaseClient.h>
#include "FS.h"
#include <LittleFS.h>
#include <vector> // Include the vector library
// --- Configuration ---
// Network Credentials
#define WIFI_SSID "insert"
#define WIFI_PASSWORD "insert"
// Firebase Credentials
#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
// Authentication
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)
// --- 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();
// --- LittleFS Helper Functions ---
void writeFile(fs::FS &fs, const char * path, const char * message){
// Serial.printf("Writing file: %s\r\n", path); // Commented out to reduce verbose output
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){
// Serial.printf("Appending to file: %s\r\n", path); // Commented out to reduce verbose output
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();
}
// --- Modified readFile Function for new entries ---
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 (or it's a directory).");
return;
}
String currentLine = "";
bool readingNewEntries = false;
// Clear the vector before populating it with new entries from this read cycle
newEntriesToProcess.clear();
Serial.println("- newEntriesToProcess vector cleared for this cycle.");
while(file.available()){
char c = file.read();
currentLine += c;
if (c == '\n') { // End of a line/entry
if (currentLine.indexOf("stopread") != -1) {
Serial.println("--- 'stopread' marker found. Processing new entries from this point. ---");
readingNewEntries = true;
} else if (readingNewEntries) {
// This is a new entry line after the 'stopread' marker or if marker not yet found
MinerDataEntry entry; // Create a new struct instance for this entry
int lastCommaIndex = -1;
int fieldCount = 0;
// Parse the line into individual fields
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(); // Remove any leading/trailing whitespace
// Assign to the correct field in the struct
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;
case 5:
entry.country = field;
entry.country.replace("\r", ""); // Clean up potential carriage return
break;
}
fieldCount++;
lastCommaIndex = i;
}
}
// Add the parsed entry to the global vector if valid
if (fieldCount >= 6) {
newEntriesToProcess.push_back(entry);
Serial.println(" --> New entry parsed and added to queue. Details:");
// Verification prints
Serial.printf(" Entry Num: %s\n", entry.entryNum.c_str());
Serial.printf(" Danger: %s\n", entry.dangerStatus.c_str());
Serial.printf(" Gas PPM: %.2f\n", entry.gasPPM);
Serial.printf(" Miner ID: %s\n", entry.minerId.c_str());
Serial.printf(" Timestamp: %s\n", entry.timestamp.c_str());
Serial.printf(" Country: %s\n", entry.country.c_str());
Serial.println(" -------------------------------------");
} else {
Serial.printf(" Malformed entry (skipped): %s\n", currentLine.c_str());
}
}
currentLine = ""; // Reset for the next line
}
}
file.close(); // Close the file after reading
Serial.printf("- Finished reading file. %d new entries found and queued.\n", newEntriesToProcess.size());
// Append the 'stopread' marker at the end of the file
// This marks all currently read data as processed for the next cycle.
File appendFileHandle = fs.open(path, FILE_APPEND);
if(!appendFileHandle){
Serial.println("- Failed to open file for appending 'stopread' marker.");
return;
}
if(appendFileHandle.print("stopread\n")){
Serial.println("- 'stopread' marker appended to file.");
} else {
Serial.println("- Failed to append 'stopread' marker.");
}
appendFileHandle.close();
Serial.println("--- File read and marker update complete. ---");
}
// --- 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 ---");
delay(10000); // needs to be some kind of delay
for (const auto& entry : newEntriesToProcess) {
String path = "/readings/" + entry.entryNum; // Base path for this entry
// Push each field as a child node under the entry path
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);
Database.set(aClient, path + "/country", entry.country, processFirebaseResult, "RTDB_country_" + entry.entryNum);
Serial.printf(" Sent Entry %s to Firebase.\n", entry.entryNum.c_str());
}
// After attempting to push all new entries, clear the vector.
// This assumes successful push or that you're fine with potential re-sends
// if Firebase fails for some entries. For production, you might want more
// granular success tracking.
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); // Increased baud rate for faster output
Serial.println("\n--- Starting ESP32 Setup ---");
// 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!");
// Consider what to do if LittleFS fails, e.g., halt or try again.
return;
} else {
Serial.println("LittleFS Mounted Successfully.");
}
// Write the header ONCE or append 'stopread' based on file existence
if (FORMAT_LITTLEFS_IF_FAILED || !LittleFS.exists("/data.csv")) {
Serial.println("Creating or formatting /data.csv with header.");
writeFile(LittleFS, "/data.csv", "entry,danger,gas_ppm,miner_id,timestamp,country\n");
} else {
Serial.println("File /data.csv already exists. Appending 'stopread' to mark previous data on boot.");
appendFile(LittleFS, "/data.csv", "stopread\n");
}
Serial.println("--- ESP32 Setup Complete ---");
}
// --- Loop Function ---
void loop(){
// Maintain Firebase authentication and async tasks
app.loop();
// Only proceed if Wi-Fi is connected and Firebase is ready
if (WiFi.status() == WL_CONNECTED && app.ready()){
// --- Data Generation and Logging to LittleFS ---
// You'd replace this with your actual sensor readings
float ppm = random(50, 200); // Simulate fluctuating gas ppm
static unsigned long entryCounter = 0; // Persistent counter for entries
entryCounter++;
String entry_num = "entry" + String(entryCounter);
String danger_status = (ppm > 150) ? "DANGER" : "SAFE";
String miner_id = "miner_ESP32C3";
// Using `unsigned long` for timestamp now for better uniqueness
String timestamp_str = String(millis());
String country_name = "USA";
// Construct the full entry string for logging
String dataToLog = entry_num + "," + danger_status + "," + String(ppm) + "," + miner_id + "," + timestamp_str + "," + country_name + "\n";
Serial.println("\n--- Appending New Data Entry to LittleFS ---");
appendFile(LittleFS, "/data.csv", dataToLog.c_str());
// --- Read New Data from LittleFS and Queue for Firebase ---
readAndProcessNewEntriesFromFile(LittleFS, "/data.csv");
// --- Periodic Firebase Push ---
unsigned long currentTime = millis();
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
}
}
This version is like the code above, but instead of using dummy values for entries, we're actually getting values from the circuit board. One essential thing I noticed is that the Ro needed to be changed depending on the MQ2, which requires calibrating in clean air. Entries are also recorded pushed very very fast, because for some reason, firebase will only accept every 10th push or so. Fast entry recording offsets the fact that firebase only accepts every 10th push, because the difference between the 1st and 10th entry in the csv is basically the same. I'm still not totally sure why, I think it might have something to do with firebase limiting the amount of times you can push in a certain amount of time. Also, another thing I changed in this new code is that I made the stopread marker system better, I have a CSV keeping track of the last pushed entry so there are no duplicate entries pushed to firebase.
#include <Wire.h>
#include "RTClib.h"
#include <Arduino.h>
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <FirebaseClient.h>
#include "FS.h"
#include <LittleFS.h>
#include <vector> // 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 "insert"
#define WIFI_PASSWORD "insert"
#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
}
}
Please see Week 16 for more detailed information on the development of the circuit board & the little tracker box the components are housed in. However, I will summarize here.
For a long time (few weeks), I was actually stuck on this project because of the circuit board. The code and components would only intermittently work, even though everything looked to be correct. Most of the time, when the active buzzer was introduced, everything would stop working. What I discovered is that the active buzzer absolutely needs an NPN transistor. It can't be directly connected to 5V & GND. The NPN transistor helps amplify the current in the board to the buzzer.
PCB with transistor. Pin E of the transistor is connected to the bottom left single pinheader, which is where the negative end of the active buzzer goes.
The milled board with all components. I used thick red wire and shrink tubes for some of the components, but I would recommend using thinner wire, even if it means just using jumper cables if they're the thinnest wire you've got.
Please see Week 16 for more detailed information on the print. However, I used generic PLA, and made my box in Fusion360. I measured parts such as the circuit board and the MQ2 sensor size to make holes and tabs that would neatly fit each component. In the demo, you can't even hear any rattling when I shake the box. The little knob for clipping on a carabiner is also pretty clever, you want to make sure the material is nice and thick, about .5" so it doesn't snap off.
Multiple angles of the print.
Everything fits! How cool! The microcontroller is powered by the USB-C in this photo. The XIAO is absolutely capable of being powered by a Li-Polymer battery by soldering to the + and - pads on the back. If you look closely at the circuit board, you'll see that I had cut a little rectangle in my edge cut rectangle line for easy access. However, I didn't want the database getting cluttered (there could be tens of thousands of entries by now), so I didn't solder the battery. on
Name | Description | Price (USD) |
---|---|---|
Seeed XIAO ESP32C3 (available in lab) | The microcontroller for my board | $4.99/unit |
Copper Clad PCB Laminate (available in lab) | Used for milling circuit board | $0.70/unit |
Generic PLA (available in lab) | Used for 3D printing the tracker box | I used roughly $1 for 10 hours of cumulative printing |
Socket Headers (available in lab) | For connecting components to PCB traces | $0.38/unit |
NPN Transistor (available in lab) | Amplifies current for active buzzer | $0.55/unit |
Wire (available in lab) | For attaching components to headers. | $2.95/unit |
Heat Shrink (available in lab) | For securing wire around component headers | $4.95/unit |
MQ-2 Gas and Smoke Analog Sensor 5V (bought) | Gas sensor | $2.40/unit |
USB-C Data Transfer Cable (bought) | Upload code to the XIAO microcontroller | $13.59/unit |
5V Active Buzzer Electronic Alarm Magnetic(bought) | Alerts miners of danger if gases are present | $0.70/unit |
DS3231 AT24C32 Clock Module Real Time Clock Module IIC RTC (available in lab) | Returns accurate timestamp without Wifi | $2.97/unit (incl. battery) |