ML-Powered Fire & Gas Hazard Detection
Build an Arduino sensor array with a Python Random Forest classifier for real-time hazard detection.
Introduction
30 Second Summary
Smoke detectors save lives, but traditional ones only react to a single signal. What if your detection system could combine multiple environmental readings to identify hazards before they escalate?
In this project, you will build a machine learning-powered hazard detection system that reads data from real Arduino sensors and classifies the environment as "fire", "smoke", or "neutral" in real time.
What You'll Build
A smart hazard detection system that combines an Arduino sensor array with a Python-based Random Forest classifier for real-time environmental hazard classification.
By the end of this project, you'll have:
- An Arduino sketch that reads temperature, humidity, gas, and flame data from four sensors and streams it over serial communication.
- A trained Random Forest model that classifies sensor readings into fire, smoke, or neutral categories using a synthetic dataset.
- A real-time inference pipeline with bidirectional serial communication between Python and Arduino, triggering distinct buzzer alert patterns for each hazard level.
- Secret Mission: Add a confidence threshold so the buzzer only fires when the model is 90%+ confident, and log all predictions to a CSV file for later analysis.
Are there any prerequisites?
You should have a basic understanding of connecting sensors to Arduino (wiring) and Python fundamentals. You will also need an Arduino UNO, a DHT11 sensor, an MQ2 sensor, a flame sensor, and a buzzer.
Before We Start
Before you can build a hazard detection system, your computer needs the right tools. Your Arduino will handle the hardware side, but the machine learning magic happens in Python.
In this step, you'll confirm your Python version, install the libraries needed for sensor communication and machine learning, and create a clean project folder with all the files you'll work in throughout this project.
In this step, get ready to:
- Verify your Python version is 3.11 or higher.
- Install the required Python libraries for serial communication and machine learning.
- Create your project folder and placeholder files.
Verify Python and install libraries
The libraries you need (scikit-learn and pandas) require Python 3.11 or higher. Let's confirm your version first.
- Press the Windows key to open your search bar.
- Type PowerShell and press Enter to open it.
- Check your Python version by running this command:
python --version
✔️ I see version 3.11 or higher
Your Python version is compatible. You're ready to install the project libraries.
ⓧ I see an older version
You have Python installed, but the version is too old for the libraries this project uses. You need to install Python 3.11 or higher.
- Go to https://www.python.org/downloads/ and download the latest Python release.
- Run the installer. Make sure to check the box that says Add python.exe to PATH before clicking Install Now.
- Close and reopen PowerShell, then run python --version again to confirm.
Older version still showing?
Windows sometimes has a Microsoft Store alias for python that overrides your new install. In Settings, search for Manage app execution aliases and turn off the Python aliases.
ⓧ Command not found
Python is not installed on your machine yet.
- Go to https://www.python.org/downloads/ and download the latest Python release.
- Run the installer. Make sure to check the box that says Add python.exe to PATH before clicking Install Now.
- Close and reopen PowerShell, then run python --version to confirm you see 3.11 or higher.
- Install all the required libraries by running this command:
pip install pyserial scikit-learn pandas numpy joblib
What are these libraries?
Each library handles a different part of the project:
- pyserial lets Python talk to your Arduino over a USB serial connection.
- scikit-learn provides the Random Forest classifier you'll train.
- pandas and numpy handle data manipulation and number crunching.
- joblib saves and loads your trained model to a file.
- Wait for the installation to finish. You should see Successfully installed messages for each package.
Seeing a permission error?
Try running the install command with the --user flag:
pip install --user pyserial scikit-learn pandas numpy joblib
If pip itself is not recognized, make sure you checked Add python.exe to PATH during the Python installation. Help me fix my pip installation.
Create the project folder
A clean folder structure keeps your Arduino code, dataset generation, model training, and inference scripts organized in one place.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir fire-detection
cd fire-detection
- Create the placeholder files for each part of the project by running this command:
New-Item -ItemType "File" -Path "generate_dataset.py", "train_model.py", "realtime_inference.py", "sensor_reader.ino"
- Confirm the files exist by running:
ls
- You should see generate_dataset.py, train_model.py, realtime_inference.py, and sensor_reader.ino listed in the output.
Why these four files?
Each file handles one stage of the project pipeline:
- sensor_reader.ino is the Arduino sketch that reads your physical sensors.
- generate_dataset.py creates the synthetic training data for your ML model.
- train_model.py trains and saves the Random Forest classifier.
- realtime_inference.py runs live predictions and sends commands back to the Arduino.
Seeing an error about the path?
Make sure you ran cd ~/Desktop first and that the mkdir fire-detection command completed without errors. If PowerShell says the folder already exists, run cd fire-detection and try the New-Item command again. Help me create my project files.
Test library imports
A quick test confirms that every library installed correctly and can be imported without errors.
- Verify all libraries are working by running this command:
python -c "import serial; import sklearn; import pandas; import numpy; import joblib; print('All libraries imported successfully!')"
- You should see All libraries imported successfully! printed in your terminal.
Seeing a ModuleNotFoundError?
This means one of the libraries did not install correctly. Check the error message to see which module is missing, then reinstall it individually:
pip install pyserial
Replace pyserial with whichever package name the error mentions. Note that you install pyserial but import it as serial. Help me fix my import error.
Your environment is ready. Next up, you'll wire the sensors to your Arduino and upload the sketch that streams live data over serial.
Set Up Python Libraries
Your hazard detection system needs Python to run the machine learning side of things. Python will handle generating training data, training the model, and making real-time predictions from live sensor readings.
In this step, you'll confirm your Python version meets the project's requirements, install the five libraries you need, and set up the project folder structure.
In this step, get ready to:
- Confirm Python 3.11 or higher is installed and install required libraries.
- Create the project folder with placeholder files for each script.
- Verify all libraries import correctly.
Verify your Python version
This project uses scikit-learn and pandas, both of which require Python 3.11 or higher. Before installing anything, confirm your Python version meets this requirement.
macOS
- Open your terminal by pressing Cmd+Space, typing Terminal, and pressing Enter.
- Check your Python version by running this command:
python3 --version
Windows
- Open your terminal by pressing the Windows key, typing PowerShell, and pressing Enter.
- Check your Python version by running this command:
python --version
✔️ I see version 3.11 or higher
Your Python version is good to go. Move on to installing the required libraries below.
ⓧ I see an older version
You have Python installed, but it is an older version that will not work with this project's dependencies.
- Go to python.org/downloads and download Python 3.11 or higher.
- Run the installer. On Windows, check the box that says Add python.exe to PATH before clicking Install Now.
- Close and reopen your terminal, then run the version check command again to confirm.
Still seeing the old version?
On Windows, the old version may shadow the new one in your PATH. Try running py --version instead. If that shows the correct version, use py in place of python throughout this project.
On macOS, the system Python is often an older version. After installing a newer Python, use python3.11 directly or set up a version manager.
help me fix my Python version
ⓧ Command not found
Python is not installed on your machine yet.
- Go to python.org/downloads and download Python 3.11 or higher.
- Run the installer. On Windows, check the box that says Add python.exe to PATH before clicking Install Now.
- Close and reopen your terminal, then run the version check command again to confirm it shows 3.11 or higher.
Installer not working?
Make sure you download the correct installer for your system (64-bit for most modern machines). If you see a permissions error, try running the installer as Administrator.
help me install Python
Install required libraries
Your project needs five Python libraries. Each one handles a different part of the machine learning pipeline.
- Install all five libraries at once by running this command:
pip install pyserial scikit-learn pandas numpy joblib
What do these libraries do?
- pyserial lets Python talk to your Arduino over a USB serial connection.
- scikit-learn provides the Random Forest classifier that will detect hazards.
- pandas makes it easy to work with your sensor dataset as tables.
- numpy handles the math behind generating synthetic sensor data.
- joblib saves and loads your trained model to disk.
Wait for the installation to complete. You should see Successfully installed messages for each package in your terminal output.
Seeing a permissions error?
If you see an error about permissions, try adding --user to the end of your pip command. If pip is not found, try pip3 instead.
help me fix my pip installation
Create the project folder structure
Your project needs four files: an Arduino sketch, a dataset generator, a model trainer, and a real-time inference script. You'll set up the folder and create empty placeholder files now, then fill them in over the next steps.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir hazard-detection
cd hazard-detection
- Create the four placeholder files by running this command:
macOS
touch generate_dataset.py train_model.py realtime_inference.py sensor_reader.ino
Windows
New-Item -ItemType File -Path "generate_dataset.py", "train_model.py", "realtime_inference.py", "sensor_reader.ino"
- Confirm the files exist by running:
ls
You should see generate_dataset.py, train_model.py, realtime_inference.py, and sensor_reader.ino in the output.
What is each file for?
- generate_dataset.py will create a synthetic training dataset with labeled sensor readings.
- train_model.py will train the Random Forest classifier on that dataset.
- realtime_inference.py will read live Arduino data, run predictions, and send commands back.
- sensor_reader.ino is the Arduino sketch that reads sensors and handles buzzer alerts.
Don't see all four files?
Make sure you are inside the hazard-detection folder when you run ls. If you see the folder itself listed instead, run cd hazard-detection first, then ls again.
help me find my project files
Test that all libraries import correctly
A quick import test confirms everything installed properly. If any library is missing or broken, you'll see an error here instead of later in the project.
- Run this test command to import all five libraries:
macOS
python3 -c "import serial; import numpy; import pandas; import sklearn; import joblib; print('All libraries ready!')"
Windows
python -c "import serial; import numpy; import pandas; import sklearn; import joblib; print('All libraries ready!')"
You should see All libraries ready! printed in your terminal.
Seeing a ModuleNotFoundError?
This means one of the libraries did not install correctly. Check which module name appears in the error and reinstall it individually.
A common issue is that pip installed the library for a different Python version than the one you are running. Make sure you use the same Python (python or python3) for both installing and running.
help me fix my library imports
Your Python environment is ready and your project folder is set up. Next up, you'll wire your sensors to the Arduino and upload the sketch that reads environmental data.
Wire Sensors and Upload Arduino Sketch
Your Python environment is set up with all the libraries you need. Now it's time to build the hardware side of your detection system.
The Arduino UNO will read temperature, humidity, gas, and flame data from three sensors and stream it to your computer over serial communication. For your Python ML model to work later, it needs clean, comma-separated data arriving at a predictable rate.
In this step, get ready to:
- Wire the DHT11, MQ2, flame sensor, and buzzer to your Arduino UNO.
- Install the Adafruit DHT library and upload the sensor reader sketch.
- Verify that all four sensor values are streaming correctly in the Serial Monitor.
Wire the DHT11, MQ2, flame sensor, and buzzer
Each sensor needs a data connection to a specific Arduino pin, plus power (5V) and ground (GND). Here are the four connections you need to make.
- Connect the DHT11 data pin to digital pin 2 on the Arduino. Connect its VCC pin to 5V and GND pin to GND.
- Connect the MQ2 analog output pin to A0 on the Arduino. Connect its VCC to 5V and GND to GND.
- Connect the flame sensor analog output pin to A1 on the Arduino. Connect its VCC to 5V and GND to GND.
- Connect the buzzer positive leg to digital pin 8 and the negative leg to GND.
Why these specific pins?
The MQ2 and flame sensor output variable voltage levels that represent gas concentration and flame intensity. Analog pins (A0, A1) can read these voltage levels as values between 0 and 1023.
The DHT11 uses a digital protocol to send temperature and humidity, so it connects to a digital pin. The buzzer just needs to be switched on or off, so a digital pin works for it too.
MQ2 warm-up time
The MQ2 gas sensor needs about 20 seconds of warm-up time after powering on before its readings stabilize. Let it run for a moment before relying on its output.
Install the DHT library and write the Arduino sketch
The DHT11 sensor uses a proprietary communication protocol. The Adafruit DHT sensor library handles this protocol so you can read temperature and humidity with simple function calls.
- In the Arduino IDE, click **Sketch** in the top menu bar.
- Select Include Library, then click Manage Libraries... to open the Library Manager.
- Type DHT sensor library in the search field.
- Find the entry labeled DHT sensor library by Adafruit and click Install.
- A dialog will appear asking to install dependencies. Click Install All to also install the required Adafruit Unified Sensor library.
What is a library in Arduino?
A library is pre-written code that simplifies working with specific hardware or protocols. Instead of writing low-level timing code to communicate with the DHT11, you can call dht.readTemperature() and get the value directly.
Now open your sensor_reader.ino file in the Arduino IDE. You will add the code in three sections: initialization, the main loop, and buzzer functions.
- Add the library includes, pin definitions, and setup function by pasting this code into sensor_reader.ino:
#include <DHT.h>
#define DHTPIN 2
#define DHTTYPE DHT11
#define MQ2PIN A0
#define FLAMEPIN A1
#define BUZZERPIN 8
DHT dht(DHTPIN, DHTTYPE);
void setup() {
Serial.begin(9600);
dht.begin();
pinMode(BUZZERPIN, OUTPUT);
digitalWrite(BUZZERPIN, LOW);
}
What does this code do?
The #define lines assign friendly names to each pin number so the rest of the code is readable. DHT dht(DHTPIN, DHTTYPE) creates a sensor object that knows which pin and sensor type to use.
In setup(), the serial port opens at 9600 baud for communication with Python. The DHT sensor initializes, and the buzzer pin is set as an output starting in the OFF state.
- Add the main loop function directly below the setup() function:
void loop() {
float temperature = dht.readTemperature();
float humidity = dht.readHumidity();
int gasValue = analogRead(MQ2PIN);
int flameValue = analogRead(FLAMEPIN);
if (isnan(temperature) || isnan(humidity)) {
Serial.println("ERROR");
} else {
Serial.print(temperature);
Serial.print(",");
Serial.print(humidity);
Serial.print(",");
Serial.print(gasValue);
Serial.print(",");
Serial.println(flameValue);
}
if (Serial.available() > 0) {
char command = Serial.read();
if (command == 'F') {
fireBuzzer();
} else if (command == 'S') {
smokeBuzzer();
} else if (command == 'N') {
digitalWrite(BUZZERPIN, LOW);
}
}
delay(2000);
}
What does the loop do?
Every 2 seconds, the loop reads all four sensor values. If the DHT11 returns valid data, it prints temperature, humidity, gas, and flame as a comma-separated line over serial. If the DHT11 fails, it prints ERROR so your Python script knows to skip that reading.
The second if block checks for incoming commands from Python. Later, your inference script will send F, S, or N to trigger fire alerts, smoke alerts, or silence the buzzer.
- Add the two buzzer functions below the loop() function:
void fireBuzzer() {
for (int i = 0; i < 3; i++) {
digitalWrite(BUZZERPIN, HIGH);
delay(200);
digitalWrite(BUZZERPIN, LOW);
delay(100);
}
}
void smokeBuzzer() {
digitalWrite(BUZZERPIN, HIGH);
delay(500);
digitalWrite(BUZZERPIN, LOW);
delay(500);
}
What do the buzzer functions do?
Each function produces a distinct alert pattern. fireBuzzer() plays three rapid beeps (200ms on, 100ms off) to signal an urgent fire detection.
smokeBuzzer() plays a single long beep (500ms on, 500ms off) as an early smoke warning. These distinct patterns let you identify the hazard type by sound alone.
- Make sure your Arduino UNO is connected to your computer via USB.
- In the Arduino IDE, click Tools in the top menu bar, then select Board and choose Arduino Uno.
- Click Tools again, then select Port and choose the COM port that shows your Arduino (e.g. COM3).
- Upload the sketch by clicking the Upload button (right arrow icon) in the toolbar.
You should see "Done uploading." in the console output at the bottom of the Arduino IDE.
Upload failed?
Make sure you selected the correct board (Arduino Uno) and port under the Tools menu. If no port appears, try a different USB cable or USB port on your computer.
If you see a compilation error mentioning DHT.h, the library did not install correctly. Go back to Library Manager and confirm that both "DHT sensor library" and "Adafruit Unified Sensor" show as installed.
Help me fix my Arduino upload error
✔️ Awesome, I've got everything!
Great. Double check you have saved your file before moving on.
ⓧ I'd like to double check the full code
#include <DHT.h>
#define DHTPIN 2
#define DHTTYPE DHT11
#define MQ2PIN A0
#define FLAMEPIN A1
#define BUZZERPIN 8
DHT dht(DHTPIN, DHTTYPE);
void setup() {
Serial.begin(9600);
dht.begin();
pinMode(BUZZERPIN, OUTPUT);
digitalWrite(BUZZERPIN, LOW);
}
void loop() {
float temperature = dht.readTemperature();
float humidity = dht.readHumidity();
int gasValue = analogRead(MQ2PIN);
int flameValue = analogRead(FLAMEPIN);
if (isnan(temperature) || isnan(humidity)) {
Serial.println("ERROR");
} else {
Serial.print(temperature);
Serial.print(",");
Serial.print(humidity);
Serial.print(",");
Serial.print(gasValue);
Serial.print(",");
Serial.println(flameValue);
}
if (Serial.available() > 0) {
char command = Serial.read();
if (command == 'F') {
fireBuzzer();
} else if (command == 'S') {
smokeBuzzer();
} else if (command == 'N') {
digitalWrite(BUZZERPIN, LOW);
}
}
delay(2000);
}
void fireBuzzer() {
for (int i = 0; i < 3; i++) {
digitalWrite(BUZZERPIN, HIGH);
delay(200);
digitalWrite(BUZZERPIN, LOW);
delay(100);
}
}
void smokeBuzzer() {
digitalWrite(BUZZERPIN, HIGH);
delay(500);
digitalWrite(BUZZERPIN, LOW);
delay(500);
}
Verify sensor output in the Serial Monitor
With the sketch uploaded, the Arduino is now reading sensors and streaming data every 2 seconds. You need to confirm all four values are arriving correctly before building the ML pipeline.
- In the Arduino IDE, click Tools in the top menu bar.
- Click Serial Monitor to open the monitor panel.
- Make sure the baud rate dropdown in the Serial Monitor is set to 9600.
You should see lines appearing every 2 seconds in this format:
25.40,48.00,167,892
Each line contains four comma-separated values: temperature (Celsius), humidity (percentage), gas level (0-1023), and flame sensor reading (0-1023).
What do these numbers mean?
Temperature and humidity come from the DHT11 as decimal values. The gas value from the MQ2 increases when more combustible gas or smoke is present.
The flame sensor reading decreases when it detects infrared light from a flame. A high value (around 900) means no flame, while a low value (around 300) means flame detected.
Seeing ERROR or no output?
If you see ERROR printing repeatedly, the DHT11 is not responding. Double-check that the DHT11 data pin is connected to digital pin 2 and that VCC and GND are connected.
If you see no output at all, confirm the baud rate is set to 9600 in the Serial Monitor dropdown. Also check that the correct COM port is selected under Tools > Port.
Help me debug my Arduino sensor output
Close Serial Monitor before running Python
Only one program can access the Arduino's COM port at a time. When you run the Python inference script later, you will need to close the Serial Monitor first.
Your Arduino is now a fully functional sensor hub streaming live environmental data. Next up, you will generate a synthetic training dataset that teaches your ML model what fire, smoke, and neutral conditions look like.
Generate Synthetic Training Dataset
Your Arduino is streaming live sensor data over serial. Now you need a dataset to teach your machine learning model what each hazard class looks like.
Collecting thousands of real fire readings would be dangerous and impractical. Instead, you will generate a synthetic dataset that mimics realistic sensor behavior for each class (neutral, smoke, fire) using Gaussian noise. This gives the model enough variety to learn meaningful patterns without needing actual emergencies.
In this step, get ready to:
- Generate 1500 labeled samples (500 per class) with realistic sensor value ranges.
- Save the dataset as a CSV file and verify the class distributions.
- Explore the feature ranges to confirm the data looks reasonable.
Define sensor ranges for each hazard class
Each hazard class has a distinct sensor signature. Normal rooms have moderate temperature and low gas. Smoke conditions show elevated gas before visible flames appear. Active fires push temperature high and trigger the flame sensor.
- Open generate_dataset.py in your code editor.
- Add the following code that defines realistic sensor value ranges for each class and generates 500 samples per class using random normal distributions:
import numpy as np
import pandas as pd
np.random.seed(42)
samples_per_class = 500
# Neutral: normal room conditions
neutral_temp = np.random.normal(25, 3, samples_per_class)
neutral_humidity = np.random.normal(50, 10, samples_per_class)
neutral_gas = np.random.normal(150, 30, samples_per_class)
neutral_flame = np.random.normal(900, 50, samples_per_class)
# Smoke (early detection): elevated gas, slightly warm
smoke_temp = np.random.normal(30, 4, samples_per_class)
smoke_humidity = np.random.normal(40, 8, samples_per_class)
smoke_gas = np.random.normal(450, 60, samples_per_class)
smoke_flame = np.random.normal(750, 80, samples_per_class)
# Fire: high temp, high gas, low flame sensor value (flame detected)
fire_temp = np.random.normal(45, 5, samples_per_class)
fire_humidity = np.random.normal(25, 5, samples_per_class)
fire_gas = np.random.normal(700, 80, samples_per_class)
fire_flame = np.random.normal(300, 100, samples_per_class)
What does this code do?
Each np.random.normal(mean, std_dev, count) call generates 500 values clustered around a realistic center point. The standard deviation adds natural variation, so no two samples are identical.
The flame sensor outputs LOW values when fire is detected and HIGH values when no fire is present. That is why the fire class uses a mean of 300 and the neutral class uses 900.
Setting np.random.seed(42) ensures you get the same random numbers every time you run the script. This makes your results reproducible.
Why synthetic data instead of real sensor readings?
Collecting real fire and smoke data requires controlled burn conditions and safety equipment. Synthetic data lets you train a working model immediately. The value ranges here are based on real-world DHT11, MQ2, and flame sensor readings documented in Arduino community projects.
- Save generate_dataset.py.
You will not see any output yet. The next chunk assembles these arrays into a dataset and saves it.
Assemble the dataset and save to CSV
Now you will combine all three classes into a single pandas DataFrame, shuffle the rows so the model does not learn the order, and export to a CSV file.
- In generate_dataset.py, add the following code below the fire class generation block:
temperatures = np.concatenate([neutral_temp, smoke_temp, fire_temp])
humidities = np.concatenate([neutral_humidity, smoke_humidity, fire_humidity])
gas_values = np.concatenate([neutral_gas, smoke_gas, fire_gas])
flame_values = np.concatenate([neutral_flame, smoke_flame, fire_flame])
labels = np.array(["neutral"] * samples_per_class + ["smoke"] * samples_per_class + ["fire"] * samples_per_class)
df = pd.DataFrame({
"temperature": temperatures,
"humidity": humidities,
"gas": gas_values,
"flame": flame_values,
"label": labels
})
df = df.sample(frac=1, random_state=42).reset_index(drop=True)
df.to_csv("sensor_data.csv", index=False)
print(f"Dataset saved: {len(df)} samples")
print(df["label"].value_counts())
print("\nFeature ranges:")
print(df.describe())
What does this code do?
- np.concatenate joins the 500-sample arrays from each class into single 1500-element arrays for each sensor.
- The labels array assigns the correct class name to each sample in the same order they were concatenated.
- df.sample(frac=1) shuffles all rows randomly so the model cannot memorize the class order during training.
- df.to_csv("sensor_data.csv", index=False) saves the full dataset to disk without row numbers.
- Save generate_dataset.py.
✔️ Awesome, I've got everything!
Great. Double check you have saved your file before running it in the next substep.
ⓧ I'd like to double check the full code
Here is the complete generate_dataset.py file:
import numpy as np
import pandas as pd
np.random.seed(42)
samples_per_class = 500
# Neutral: normal room conditions
neutral_temp = np.random.normal(25, 3, samples_per_class)
neutral_humidity = np.random.normal(50, 10, samples_per_class)
neutral_gas = np.random.normal(150, 30, samples_per_class)
neutral_flame = np.random.normal(900, 50, samples_per_class)
# Smoke (early detection): elevated gas, slightly warm
smoke_temp = np.random.normal(30, 4, samples_per_class)
smoke_humidity = np.random.normal(40, 8, samples_per_class)
smoke_gas = np.random.normal(450, 60, samples_per_class)
smoke_flame = np.random.normal(750, 80, samples_per_class)
# Fire: high temp, high gas, low flame sensor value (flame detected)
fire_temp = np.random.normal(45, 5, samples_per_class)
fire_humidity = np.random.normal(25, 5, samples_per_class)
fire_gas = np.random.normal(700, 80, samples_per_class)
fire_flame = np.random.normal(300, 100, samples_per_class)
temperatures = np.concatenate([neutral_temp, smoke_temp, fire_temp])
humidities = np.concatenate([neutral_humidity, smoke_humidity, fire_humidity])
gas_values = np.concatenate([neutral_gas, smoke_gas, fire_gas])
flame_values = np.concatenate([neutral_flame, smoke_flame, fire_flame])
labels = np.array(["neutral"] * samples_per_class + ["smoke"] * samples_per_class + ["fire"] * samples_per_class)
df = pd.DataFrame({
"temperature": temperatures,
"humidity": humidities,
"gas": gas_values,
"flame": flame_values,
"label": labels
})
df = df.sample(frac=1, random_state=42).reset_index(drop=True)
df.to_csv("sensor_data.csv", index=False)
print(f"Dataset saved: {len(df)} samples")
print(df["label"].value_counts())
print("\nFeature ranges:")
print(df.describe())
Run the script and explore feature ranges
- Run the dataset generation script by executing this command in your terminal:
python generate_dataset.py
- You should see output showing Dataset saved: 1500 samples followed by the class counts (500 each for fire, neutral, smoke) and a statistics table.
- Confirm that sensor_data.csv now exists in your project folder by running:
ls
You should see sensor_data.csv listed alongside your Python scripts.
What do the feature ranges tell you?
The df.describe() output shows statistics across all 1500 samples combined. Look at the min and max values for each feature.
- Temperature ranges from ~15 to ~60, spanning all three classes.
- Gas values range from ~50 to ~900, with a clear separation between neutral (~150) and fire (~700).
- Flame values are inverted. Low values mean flame detected. High values mean no flame.
These clear separations between classes are what will allow the Random Forest model to achieve high accuracy in the next step.
Seeing a ModuleNotFoundError?
If Python cannot find numpy or pandas, the libraries may not be installed in the Python version you are running. Make sure you are using the same Python that you installed packages with in Step 1.
help me fix a ModuleNotFoundError when running generate_dataset.py
You now have 1500 labeled training samples sitting in sensor_data.csv. Next up, you will feed this data into a Random Forest classifier and train a model that can predict hazard classes from raw sensor readings.
Train the Random Forest Model
Your synthetic dataset is ready with 1500 labeled sensor readings. Now it's time to teach a machine learning model to recognize the difference between neutral, smoke, and fire conditions.
A Random Forest classifier works by building many decision trees and combining their predictions. It's lightweight, interpretable, and achieves high accuracy on sensor fusion data. In this step, you'll train one on your dataset and evaluate how well it distinguishes between hazard classes.
In this step, get ready to:
- Load the dataset and split it into training and testing sets.
- Train a Random Forest model and evaluate its accuracy.
- Save the trained model and inspect feature importances.
Load the dataset and split into training and testing sets
Before training, you need to separate your data into features (the sensor readings) and labels (the hazard class). You also need to hold back 20% of the data for testing so you can measure how well the model performs on data it hasn't seen.
- Open train_model.py in your text editor.
- Add the following code to import libraries, load the CSV, and split the data:
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import classification_report
import joblib
# Load the synthetic dataset
df = pd.read_csv("sensor_data.csv")
# Separate features (sensor readings) from labels (hazard class)
X = df[["temperature", "humidity", "gas", "flame"]]
y = df["label"]
# Split into 80% training, 20% testing with stratification
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42, stratify=y
)
What does this code do?
pd.read_csv("sensor_data.csv") loads the 1500-sample dataset you generated in the previous step.
X holds the four sensor columns (what the model learns from). y holds the label column (what the model tries to predict).
The stratify=y parameter ensures that each split keeps the same proportion of neutral, smoke, and fire samples. Without it, one class could be over-represented in testing.
- Save train_model.py.
Train the classifier and evaluate performance
With data split into training and testing sets, you can now train the Random Forest on the training portion and check how accurately it classifies the held-out test data.
- Add this code below the train_test_split call in train_model.py:
# Train the Random Forest with 100 decision trees
clf = RandomForestClassifier(n_estimators=100, random_state=42)
clf.fit(X_train, y_train)
# Evaluate on the held-out test set
y_pred = clf.predict(X_test)
print("Classification Report:")
print(classification_report(y_test, y_pred))
What does this code do?
RandomForestClassifier(n_estimators=100) creates a forest of 100 decision trees. Each tree learns slightly different patterns from the data, and the final prediction is a majority vote across all trees.
classification_report prints precision, recall, and F1-score for each class. Precision tells you how often a predicted label is correct. Recall tells you how many of the actual labels were found.
- Save train_model.py.
Save the trained model and inspect feature importances
The trained model lives in memory right now. You need to save it to a file so your real-time inference script can load it later without retraining every time.
- Add this code at the bottom of train_model.py:
# Show which sensors matter most to the model
print("\nFeature Importances:")
for name, importance in zip(X.columns, clf.feature_importances_):
print(f" {name}: {importance:.4f}")
# Save the trained model to disk
joblib.dump(clf, "hazard_model.joblib")
print("\nModel saved as hazard_model.joblib")
What does this code do?
clf.feature_importances_ shows how much each sensor contributes to the model's predictions. A higher value means that sensor is more important for distinguishing between classes.
joblib.dump(clf, "hazard_model.joblib") serializes the entire trained model to a file. Your real-time script will reload this file instead of retraining from scratch.
- Save train_model.py.
- Run the script by executing this command in your terminal:
python train_model.py
- You should see a classification report with precision, recall, and F1-score for each class, followed by the feature importances and a confirmation that the model was saved.
What should I see?
The classification report should show high accuracy (close to 1.00) for all three classes. This is expected because your synthetic data has clear separation between neutral, smoke, and fire conditions.
Feature importances will vary, but the gas and flame sensors typically rank highest because they have the largest value differences between classes.
Seeing a ModuleNotFoundError?
Make sure you installed all the required libraries in Step 1. Run pip install scikit-learn pandas joblib and try again.
Also confirm that sensor_data.csv exists in the same folder as train_model.py. If you see a FileNotFoundError, run generate_dataset.py again first.
Help me debug my train_model.py script
- Confirm that hazard_model.joblib now exists in your project folder by running:
ls
You should see hazard_model.joblib listed alongside your other project files.
✔️ Awesome, I've got everything!
Great. Double check you've saved train_model.py and that hazard_model.joblib exists in your project folder.
ⓧ I'd like to double check the full code
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import classification_report
import joblib
df = pd.read_csv("sensor_data.csv")
X = df[["temperature", "humidity", "gas", "flame"]]
y = df["label"]
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42, stratify=y
)
clf = RandomForestClassifier(n_estimators=100, random_state=42)
clf.fit(X_train, y_train)
y_pred = clf.predict(X_test)
print("Classification Report:")
print(classification_report(y_test, y_pred))
print("\nFeature Importances:")
for name, importance in zip(X.columns, clf.feature_importances_):
print(f" {name}: {importance:.4f}")
joblib.dump(clf, "hazard_model.joblib")
print("\nModel saved as hazard_model.joblib")
Why Random Forest for this project?
Random Forest is an excellent fit for sensor fusion classification. It handles the different value ranges from each sensor without needing data normalization. It also provides built-in feature importance scores, so you can see exactly which sensor drives each prediction.
Research has shown Random Forest achieves approximately 98.6% accuracy for fire risk classification with embedded sensors. It's lightweight enough to run predictions in real-time on each incoming sensor reading.
Your trained model is saved and ready to make predictions. Next up, you'll connect it to live Arduino sensor data and build a real-time inference system that triggers buzzer alerts based on the model's predictions.
Build the Real-Time Inference System
Your trained model sits in hazard_model.joblib, ready to classify sensor readings. Your Arduino is streaming live data over serial. The missing piece is a bridge that connects these two worlds in real time.
In this step, you will write a Python script that reads live sensor data from the Arduino, feeds it into the trained Random Forest model for prediction, and sends a command character back to trigger distinct buzzer alerts. This creates a full bidirectional communication loop between your computer and the Arduino.
In this step, get ready to:
- Write a Python script that connects to the Arduino serial port and parses incoming CSV sensor data into feature arrays.
- Load the saved model and run predictions with confidence scores on each reading.
- Send single-character commands back to Arduino to trigger distinct buzzer patterns for fire, smoke, or neutral.
Connect to Arduino and parse serial data
The inference script needs to open a serial connection to the same COM port your Arduino is using, then continuously read and parse the CSV lines it sends.
What is bidirectional serial communication?
Your Arduino sends sensor data TO your computer over the USB cable. Your Python script can also send data BACK to the Arduino on the same connection. This two-way channel lets your computer act as the "brain" that decides what alert to trigger.
Close the Serial Monitor first
Only one program can use a COM port at a time. If the Arduino IDE Serial Monitor is still open from Step 2, close it now. Your Python script cannot connect while the Serial Monitor holds the port.
- Open realtime_inference.py in your text editor and paste the following code:
import serial
import joblib
import numpy as np
import time
MODEL_PATH = "hazard_model.joblib"
SERIAL_PORT = "COM3"
BAUD_RATE = 9600
clf = joblib.load(MODEL_PATH)
print(f"Model loaded from {MODEL_PATH}")
ser = serial.Serial(SERIAL_PORT, BAUD_RATE, timeout=2)
time.sleep(2)
print(f"Connected to {SERIAL_PORT}")
print("Starting real-time hazard detection...")
print("-" * 50)
try:
while True:
line = ser.readline().decode("utf-8").strip()
if not line or line == "ERROR":
continue
try:
values = line.split(",")
if len(values) != 4:
continue
temperature = float(values[0])
humidity = float(values[1])
gas = int(values[2])
flame = int(values[3])
except (ValueError, IndexError):
continue
What does this code do?
- SERIAL_PORT = "COM3" sets the COM port where your Arduino is connected. Change this to match your actual port if it differs.
- serial.Serial(SERIAL_PORT, BAUD_RATE, timeout=2) opens the serial connection at 9600 baud to match the Arduino sketch.
- time.sleep(2) gives the Arduino time to reset after the serial connection opens.
- ser.readline().decode("utf-8").strip() reads one line of CSV data from the Arduino and converts it from bytes to a clean string.
- The inner try/except block splits the CSV line into 4 values and converts them to numbers. If anything goes wrong (garbled data, incomplete line), it skips to the next reading.
Find your COM port
If you are not sure which COM port your Arduino uses, open Device Manager on Windows and expand Ports (COM & LPT). You will see your Arduino listed with its COM number (e.g. COM3, COM4). Update the SERIAL_PORT value in the script to match.
- Save the file. You will not run it yet because it is incomplete.
Load the model and run predictions
With parsed sensor values in hand, the script needs to shape them into a feature array the model understands, run a prediction, and display the result with a confidence score.
- In realtime_inference.py, find the last line that says except (ValueError, IndexError): followed by continue.
- Add the following code directly below that, still inside the outer while loop (indented by 8 spaces):
features = np.array([[temperature, humidity, gas, flame]])
prediction = clf.predict(features)[0]
probabilities = clf.predict_proba(features)[0]
confidence = max(probabilities)
print(f"Temp: {temperature:.1f}C | Humidity: {humidity:.1f}% | "
f"Gas: {gas} | Flame: {flame} | "
f"Prediction: {prediction.upper()} ({confidence:.1%})")
What does this code do?
- np.array([[temperature, humidity, gas, flame]]) wraps the four sensor values into a 2D array. Scikit-learn expects this shape for predictions.
- clf.predict(features)[0] returns the predicted class label ("fire", "smoke", or "neutral").
- clf.predict_proba(features)[0] returns the probability for each class. Taking the max gives us the model's confidence in its prediction.
- The print statement formats a dashboard-style output showing all sensor values alongside the prediction and confidence percentage.
- Save realtime_inference.py. You won't see a change yet. The next chunk adds the command-sending logic that completes the system.
Send commands back to Arduino for buzzer alerts
The final piece closes the loop. After each prediction, the script sends a single character back to the Arduino over the same serial connection. The Arduino sketch you uploaded in Step 2 already listens for these characters and triggers the matching buzzer pattern.
- In realtime_inference.py, add the following code directly below the print statement you just added (still inside the while loop), followed by the KeyboardInterrupt handler:
if prediction == "fire":
ser.write(b"F")
elif prediction == "smoke":
ser.write(b"S")
else:
ser.write(b"N")
except KeyboardInterrupt:
print("\nStopping inference...")
ser.write(b"N")
ser.close()
print("Connection closed.")
What does this code do?
- ser.write(b"F") sends the byte character 'F' to the Arduino. The Arduino's fireBuzzer() function responds with 3 rapid beeps.
- ser.write(b"S") triggers the Arduino's smokeBuzzer() function, which plays a slower single beep pattern as an early warning.
- ser.write(b"N") silences the buzzer when conditions are neutral.
- The KeyboardInterrupt handler lets you press Ctrl+C to gracefully stop the script. It silences the buzzer and closes the serial connection.
How does the Arduino handle these commands?
The sensor_reader.ino sketch you uploaded in Step 2 already checks for incoming serial data with Serial.available(). When it reads 'F', it calls fireBuzzer() (3 rapid beeps). When it reads 'S', it calls smokeBuzzer() (a slow pulse). When it reads 'N', it turns the buzzer off.
- Save realtime_inference.py.
- Make sure the Arduino IDE Serial Monitor is closed.
- Run the inference script by running this command:
python realtime_inference.py
- You should see output like this in your terminal, with a new line printing every 2 seconds:
Model loaded from hazard_model.joblib
Connected to COM3
Starting real-time hazard detection...
--------------------------------------------------
Temp: 24.3C | Humidity: 52.1% | Gas: 142 | Flame: 887 | Prediction: NEUTRAL (98.7%)
- If the model detects smoke or fire conditions, you should hear the buzzer activate on your Arduino with its corresponding pattern.
- Press Ctrl+C to stop the script when you are done testing.
MQ2 sensor warm-up time
The MQ2 gas sensor needs about 20 seconds of warm-up time before its readings stabilize. If you see erratic gas values in the first few readings, wait a moment. The readings will settle down.
Not seeing any output?
- Make sure the Arduino IDE Serial Monitor is fully closed. Only one program can access the COM port at a time.
- Double check that the SERIAL_PORT value in your script matches the actual COM port shown in Device Manager.
- Confirm your Arduino is plugged in and the power LED is on. Try unplugging and reconnecting the USB cable.
- If you see a permission error, make sure no other Python process is already using the same serial port.
Help me debug my serial connection
✔️ Awesome, I've got everything!
Great. Double check you have saved realtime_inference.py and that predictions are printing to your terminal.
ⓧ I'd like to double check the full code
Here is the complete realtime_inference.py file for comparison:
import serial
import joblib
import numpy as np
import time
MODEL_PATH = "hazard_model.joblib"
SERIAL_PORT = "COM3"
BAUD_RATE = 9600
clf = joblib.load(MODEL_PATH)
print(f"Model loaded from {MODEL_PATH}")
ser = serial.Serial(SERIAL_PORT, BAUD_RATE, timeout=2)
time.sleep(2)
print(f"Connected to {SERIAL_PORT}")
print("Starting real-time hazard detection...")
print("-" * 50)
try:
while True:
line = ser.readline().decode("utf-8").strip()
if not line or line == "ERROR":
continue
try:
values = line.split(",")
if len(values) != 4:
continue
temperature = float(values[0])
humidity = float(values[1])
gas = int(values[2])
flame = int(values[3])
except (ValueError, IndexError):
continue
features = np.array([[temperature, humidity, gas, flame]])
prediction = clf.predict(features)[0]
probabilities = clf.predict_proba(features)[0]
confidence = max(probabilities)
print(f"Temp: {temperature:.1f}C | Humidity: {humidity:.1f}% | "
f"Gas: {gas} | Flame: {flame} | "
f"Prediction: {prediction.upper()} ({confidence:.1%})")
if prediction == "fire":
ser.write(b"F")
elif prediction == "smoke":
ser.write(b"S")
else:
ser.write(b"N")
except KeyboardInterrupt:
print("\nStopping inference...")
ser.write(b"N")
ser.close()
print("Connection closed.")
Your ML-powered hazard detection system is now running end-to-end. Sensor data flows from Arduino to Python, gets classified by your trained model, and triggers real-time buzzer alerts. Next up, the Secret Mission challenges you to add a confidence threshold and prediction logging for production-grade reliability.
Secret mission
Add Confidence Threshold and Prediction Logging
Your hazard detection system works, but would you trust it in a real building? Right now it fires the buzzer on every single prediction, even when the model is only 55% sure. In this secret mission, you'll add a confidence gate and a full prediction log to make the system production-grade.
Clean Up Your Resources
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely locally, so there are no ongoing costs.
Resources you used:
- Project files (generate_dataset.py, train_model.py, realtime_inference.py, sensor_reader.ino, sensor_data.csv, hazard_model.joblib, predictions_log.csv).
- Python libraries (pyserial, scikit-learn, pandas, numpy, joblib).
- Adafruit DHT sensor library in Arduino IDE.
Keep everything running
No action needed. Choose this if you plan to keep experimenting with your hazard detection system or want to tweak the model later.
- Your project files stay on your Desktop.
- Your Python libraries remain installed and available for other projects.
- The Adafruit DHT sensor library stays in Arduino IDE for future sensor work.
Pause - I'll come back to this later
Stop the inference script and disconnect your Arduino. Everything stays installed so you can resume quickly.
- If realtime_inference.py is still running, press Ctrl+C in the terminal to stop it.
- Disconnect your Arduino UNO from your computer's USB port.
- Your trained model and prediction logs will be waiting when you come back.
Delete - I don't want to use this again
Remove all project files, uninstall the Python libraries, and remove the Arduino library to start completely fresh.
- If realtime_inference.py is still running, press Ctrl+C in the terminal to stop it.
- Disconnect your Arduino UNO from your computer's USB port.
- Delete your project folder from the Desktop by running this command in your terminal:
macOS
cd ~/Desktop
rm -rf fire-detection
Windows
cd ~\Desktop
rmdir /s /q fire-detection
- Uninstall the Python libraries by running:
pip uninstall pyserial scikit-learn pandas numpy joblib -y
- Remove the Adafruit DHT sensor library from Arduino IDE. Open the Library Manager sidebar, find DHT sensor library in your installed libraries, hover over INSTALLED until it changes to REMOVE, then click it.
- Repeat the same process for the Adafruit Unified Sensor library (a dependency that was installed alongside the DHT library).
Nice Work!
Nice Work!
You just built a complete ML-powered fire and gas hazard detection system from scratch. Your Arduino sensor array now streams live environmental data into a trained Random Forest classifier that predicts hazards in real time and triggers hardware alerts.
You've learned how to:
- Build a multi-sensor Arduino array that streams CSV-formatted data over serial communication to a Python script.
- Train a Random Forest classifier on synthetic sensor data and evaluate its performance with precision, recall, and feature importances.
- Create a real-time inference pipeline with bidirectional Arduino-Python communication that triggers distinct buzzer patterns for each hazard class.
- Added confidence thresholding and prediction logging to make the system production-grade and auditable.
Ready to quiz yourself?