/*
PROJET : Système de surveillance intelligente d'un bâtiment
Par: Ricardy Casimiet et Pierre-Yves Viau-Poirier
Cours: ENM820
Sous-modules principaux :
- Température et humidité (DHT22)
- Détection de présence (PIR)
- Calcul du taux d'occupation
- Luminosité (LDR)
- Détection / estimation de gaz (MQ2)
- Affichage local sur LCD 20x4
- Transmission des données vers ThingSpeak par Wi-Fi
Environnement de développement : Wokwi + ESP32
*/
// ================================================================
// 1. BIBLIOTHÈQUES
// ================================================================
#include <math.h>
#include <DHT.h>
#include <LiquidCrystal_I2C.h>
#include <WiFi.h>
#include <HTTPClient.h>
// ================================================================
// 2. CONFIGURATION DES CAPTEURS ET DES BROCHES
// ================================================================
#define DHTTYPE DHT22
#define MQ2_PIN 34
#define PIR_PIN 5
#define OCCUPY_PIN 17
#define DHT_PIN 15
#define LIGHT_PIN 33
#define FLASH_1 18
#define FLASH_2 19
#define SERIAL_BAUD 9600
// ================================================================
// 3. LCD ET CAPTEURS
// ================================================================
LiquidCrystal_I2C lcd(0x27, 20, 4); // LCD 20 colonnes x 4 lignes
DHT dht(DHT_PIN, DHTTYPE);
// ================================================================
// 4. VARIABLES CAPTEURS
// ================================================================
// Température et humidité mesurées par le DHT22.
float tempC = 0;
float hum = 0;
// Luminosité estimée à partir de la LDR.
float lux = 0;
float lightValue = 0;
// Présence détectée par le PIR.
// 0 = aucune présence
// 1 = présence détectée
int IRVal = 0;
// ---------------------------------------------------------------
// MQ2
// ---------------------------------------------------------------
// Valeur brute provenant de l'ADC.
int gasADC = 0;
// Concentration de gaz estimée à partir de notre calibration.
float gasPPM = 0;
// ================================================================
// 5. CALCUL DU TAUX D'OCCUPATION
// ================================================================
// Compteur du nombre de secondes pendant lesquelles le PIR détecte une présence.
unsigned long occupiedSeconds = 0;
// Début de la fenêtre d'observation.
unsigned long occupationStart = 0;
// Taux d'occupation calculé en pourcentage.
float occupation = 0;
// Fenêtre d'observation de 1 minute.
// Le nombre de minutes peut être varié (ex: pour 10 minutes ce sera 10UL * 60UL * 1000UL)
const unsigned long occupationWindow = 1UL * 60UL * 1000UL;
// ================================================================
// 6. PARAMÈTRES DE LA LDR
// ================================================================
// Emprunté de https://wokwi.com/arduino/projects/305193627138654786
// Adapté à notre logique. Les paramètres GAMMA et RL10 sont ceux utilisés par le modèle
// de photoresistor de Wokwi pour estimer la luminosité de façon précise.
const float GAMMA = 0.7;
const float RL10 = 50;
// ================================================================
// 7. PARAMÈTRES DE TEMPORISATION ET SEUILS
// ================================================================
const int timeThres = 1000; // Lecture des capteurs toutes les 1000 ms (1s)
const int timeFlash = 500; // Intervalle de clignotement des LEDs en alerte
const unsigned long uploadInterval = 30000; // Intervalle d'envoi vers ThingSpeak (30s)
// Seuil d'alerte gaz (1000 ppm est utilisé comme niveau d'avertissement dans notre prototype)
const float GAS_THRESHOLD = 1000;
unsigned long prevChrono = 0;
unsigned long prevFlash = 0;
unsigned long prevUpload = 0;
// État des LEDs d'alerte
bool flasher = false;
// ================================================================
// 8. WI-FI ET THINGSPEAK
// ================================================================
const char * ssid = "Wokwi-GUEST";
const char * password = ""; // Vide pour Wokwi-GUEST
// Connexion Cloud, ThingSpeak
const char * ThingSpeakKey = "8NEPQ7YMQTJD0CXQ"; // Clé API pour ThingSpeak channel
// Prototypes de fonctions
void WifiConnect();
void flashers();
void sendToThingSpeak(float temperature, float humidity, int presence, float occupation, float luminosite, float gasPPM);
float estimateGasPPM(int adcValue);
// ================================================================
// SETUP
// ================================================================
void setup() {
pinMode(MQ2_PIN, INPUT);
pinMode(LIGHT_PIN, INPUT);
pinMode(PIR_PIN, INPUT);
pinMode(OCCUPY_PIN, OUTPUT);
pinMode(FLASH_1, OUTPUT);
pinMode(FLASH_2, OUTPUT);
lcd.init();
lcd.backlight();
Serial.begin(SERIAL_BAUD);
lcd.setCursor(0, 0);
lcd.print("Connecting to Wi-Fi");
WifiConnect();
dht.begin();
// Initialise le début de la première fenêtre d'occupation.
occupationStart = millis();
}
// ================================================================
// LOOP PRINCIPALE
// ================================================================
void loop() {
unsigned long chrono = millis();
// --------------------------------------------------------------
// LECTURE ET TRAITEMENT DES CAPTEURS (Toutes les secondes)
// --------------------------------------------------------------
if (chrono - prevChrono >= timeThres) {
prevChrono = chrono;
// Lecture des données du DHT22
hum = dht.readHumidity();
tempC = dht.readTemperature();
// Lecture analogique du MQ2 et estimation de la concentration
gasADC = analogRead(MQ2_PIN);
gasPPM = estimateGasPPM(gasADC);
// Lecture de la LDR et calcul de la luminosité en Lux
// Emprunté de https://wokwi.com/arduino/projects/305193627138654786
lightValue = analogRead(LIGHT_PIN);
float voltage = lightValue / 4095.0 * 5.0;
float resistance = 2000.0 * voltage / (1.0 - voltage / 5.0);
lux = pow(RL10 * 1e3 * pow(10, GAMMA) / resistance, (1.0 / GAMMA));
// Détection de présence PIR
IRVal = digitalRead(PIR_PIN);
// Si PIR détecte une présence pendant cette seconde, ajouter cette seconde au temps d'occupation
if (IRVal) occupiedSeconds++;
// Calcul du taux d'occupation d'une minute (60 secondes)
if (chrono - occupationStart >= occupationWindow) {
occupation = ((float)occupiedSeconds / 60.0) * 100.0;
Serial.printf("Taux d'occupation : %.2f %%\n", occupation);
// Réinitialisation pour la prochaine fenêtre de temps
occupiedSeconds = 0;
occupationStart = chrono;
}
// Contrôle de la LED d'occupation
if (IRVal)
digitalWrite(OCCUPY_PIN, HIGH);
else
digitalWrite(OCCUPY_PIN, LOW);
// Affichage des valeurs sur le moniteur série
Serial.print("Temp :"); Serial.print(tempC); Serial.print(" °C ");
Serial.print("Humidity "); Serial.print(hum); Serial.print(" % ");
Serial.print("Lux: "); Serial.print(lux);
Serial.print(" Occupation: "); Serial.print(occupation); Serial.print(" % ");
Serial.print(" Gas concentration estimee: "); Serial.print(gasPPM); Serial.println(" ppm ");
// Affichage local sur l'écran LCD 20x4
lcd.setCursor(0, 0); lcd.print("Temp. "); lcd.print(tempC); lcd.print(" C ");
lcd.setCursor(0, 1); lcd.print("Hum. "); lcd.print(hum); lcd.print("% ");
lcd.setCursor(0, 2); lcd.print("Light "); lcd.print(lux); lcd.print("lx ");
lcd.setCursor(0, 3); lcd.print("Occ. "); lcd.print(occupation); lcd.print("% ");
}
// --------------------------------------------------------------
// ALERTE GAZ (FLASHERS)
// --------------------------------------------------------------
if (gasPPM >= GAS_THRESHOLD) {
if (chrono - prevFlash >= timeFlash) {
prevFlash = chrono;
flashers();
}
}
else {
flasher = false;
digitalWrite(FLASH_1, LOW);
digitalWrite(FLASH_2, LOW);
}
// --------------------------------------------------------------
// ENVOI PÉRIODIQUE VERS THINGSPEAK
// --------------------------------------------------------------
// Les capteurs sont lus toutes les secondes mais les données ne sont pas envoyées
// au Cloud à chaque lecture car cela ferait trop d'envois inutilement.
// Nous envoyons seulement une mesure toutes les 30 secondes.
if (chrono - prevUpload >= uploadInterval) {
prevUpload = chrono;
sendToThingSpeak(tempC, hum, IRVal, occupation, lux, gasPPM);
}
}
// ================================================================
// FONCTION : CLIGNOTEMENT DES LEDS (FLASHERS)
// ================================================================
void flashers() {
flasher = !flasher;
if (flasher) {
digitalWrite(FLASH_1, HIGH);
digitalWrite(FLASH_2, LOW);
} else {
digitalWrite(FLASH_1, LOW);
digitalWrite(FLASH_2, HIGH);
}
}
// ================================================================
// FONCTION : CONNEXION WI-FI
// ================================================================
void WifiConnect() {
// Code généré initialement avec l'aide de Google Gemini
Serial.print("Connect to Wi-Fi, please wait.");
// Configure l'ESP32 en mode station
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("\nWi-Fi Connection successful!");
Serial.print("IP Address: ");
Serial.println(WiFi.localIP()); // Affiche l'adresse IP attribuée à l'ESP32
}
// ================================================================
// FONCTION : ENVOI DE DONNÉES À THINGSPEAK
// ================================================================
void sendToThingSpeak(float temperature, float humidity, int presence, float occupation, float luminosite, float gasPPM) {
// Message de signalement si le wifi ne marche pas
if (WiFi.status() != WL_CONNECTED) {
Serial.println("Wi-Fi non connecté !");
return;
}
// Création de l'objet HTTP pour communiquer avec le serveur ThingSpeak
HTTPClient http;
// Construction de l'URL avec les champs pour chaque capteur
String url = "https://api.thingspeak.com/update?api_key=";
url += ThingSpeakKey;
url += "&field1=" + String(temperature);
url += "&field2=" + String(humidity);
url += "&field3=" + String(presence);
url += "&field4=" + String(occupation);
url += "&field5=" + String(luminosite);
url += "&field6=" + String(gasPPM);
Serial.println("Envoi vers ThingSpeak...");
// Initialise la connexion HTTP avec l'URL construite
http.begin(url);
// Effectue une requête HTTP GET
int httpCode = http.GET();
// Affiche le code retourné par le serveur
Serial.print("HTTP Code : ");
Serial.println(httpCode);
// Si la requête a reçu une réponse du serveur, afficher la réponse pour le débogage
if (httpCode > 0) {
String response = http.getString();
Serial.print("Réponse ThingSpeak : ");
Serial.println(response);
}
// Ferme la connexion HTTP et libère les ressources
http.end();
}
// ================================================================
// ESTIMATION DE LA CONCENTRATION DU MQ2
// ================================================================
// Méthode inspirée et adaptée de :
// Andy @ MYBOTIC www.mybotic.com.my
// "How to Detect Concentration of Gas by Using MQ2 Sensor"
// https://www.instructables.com/How-to-Detect-Concentration-of-Gas-by-Using-MQ2-Se/
//
// Le code original utilise le rapport Rs/Ro et une relation logarithmique basée sur
// les courbes caractéristiques du MQ2.
//
// Pour notre projet, au lieu de courbes nous avons simplement utilisé des valeurs testées
// (ex: mettre 1000 ppm et observer la valeur lue à la sortie analogique) et noté cette valeur.
// La relation PPM-ADC étant non-linéaire, une interpolation logarithmique par morceaux est utilisée.
// ================================================================
float estimateGasPPM(int adcValue) {
// --------------------------------------------------------------
// Points de calibration obtenus expérimentalement dans Wokwi.
//
// adcTable : valeur lue par analogRead()
// ppmTable : concentration configurée dans le simulateur Wokwi
// --------------------------------------------------------------
const int adcTable[] = { 843, 2230, 2674, 3436, 3672, 3762, 3835, 3889, 3971, 4026, 4041 };
const float ppmTable[] = { 0.1, 4, 13, 151, 525, 1000, 1905, 3467, 12023, 54954, 100000 };
const int tableSize = sizeof(adcTable) / sizeof(adcTable[0]);
// --------------------------------------------------------------
// Gestion des valeurs situées hors de notre plage de calibration.
// --------------------------------------------------------------
if (adcValue <= adcTable[0]) {
return ppmTable[0];
}
if (adcValue >= adcTable[tableSize - 1]) {
return ppmTable[tableSize - 1];
}
// --------------------------------------------------------------
// Recherche des deux points de calibration qui encadrent
// la valeur ADC mesurée.
// --------------------------------------------------------------
for (int i = 0; i < tableSize - 1; i++) {
if (adcValue >= adcTable[i] && adcValue <= adcTable[i + 1]) {
// Position relative de la mesure entre les deux points.
float ratio = (float)(adcValue - adcTable[i]) / (float)(adcTable[i + 1] - adcTable[i]);
// ----------------------------------------------------------
// Interpolation logarithmique.
//
// Le PPM couvre plusieurs ordres de grandeur, donc une
// interpolation linéaire directe donnerait de mauvais résultats.
// ----------------------------------------------------------
float logPPM1 = log10(ppmTable[i]);
float logPPM2 = log10(ppmTable[i + 1]);
float logPPM = logPPM1 + ratio * (logPPM2 - logPPM1);
// Retour au PPM normal.
return pow(10, logPPM);
}
}
// Cette ligne ne devrait normalement jamais être atteinte.
return 0;
}