Community project
Forest Fire Detector
This forest fire detection system combines multiple environmental sensors with long-range LoRa communication to monitor conditions that indicate wildfire risk. Built around an ESP32 microcontroller, the detector measures flame presence, temperature, humidity, soil moisture, wind speed, and rainfall while logging data to a microSD card and transmitting alerts via LoRa radio to a remote base station.
The guide provides a complete wiring diagram, parts list, and step-by-step assembly instructions for building a solar-powered, battery-backed sensor node suitable for remote deployment. Firmware is included to handle sensor calibration, multi-threshold alerting (normal, watch, and alert states), and efficient power management through adaptive sleep cycles.
Wiring diagram

Gather all the parts
Assemble it in 8 steps
1. Préparer le boîtier et le mât
Placez le boîtier IP65 sur un mât stable, 3 à 5 m au-dessus des arbres, avec une vue aussi dégagée que possible. Fixez l’antenne LoRa verticalement à l’extérieur du boîtier et faites une boucle vers le bas dans chaque câble avant son entrée afin que la pluie ne puisse pas suivre le câble vers l’électronique.
- Une couverture de 5 à 10 km² est une cible à confirmer par un essai radio et une observation réelle du relief.
- Gardez le boîtier accessible pour remplacer la carte microSD et vérifier les joints.
- Ne montez pas seul en hauteur : une chute ou la chute d’un outil peut blesser gravement.
- N’installez pas le mât sous une ligne électrique ou près d’un arbre instable.
2. Installer la coupole optique sans concentrer le soleil
Centrez la demi-sphère Fresnel au-dessus de la fenêtre de la caméra et du capteur de flamme KY-026, sans fil électrique. Ajoutez un petit pare-soleil et une ventilation protégée contre la pluie pour réduire la buée.
- Vérifiez l’alignement à l’ombre avec une lumière distante ; nettoyez la coupole seulement avec un chiffon doux.
- La caméra et la coupole servent à confirmer une alerte, pas à identifier sûrement un feu très lointain.
- Une lentille Fresnel peut concentrer le soleil et chauffer fortement un point : ne la laissez jamais focaliser sur du plastique, un câble ou des feuilles sèches.
3. Câbler tous les capteurs I²C
Reliez VCC du BME280, du GY-82, de l’ADS1115, du MCP23017, du BMV080 et du DS3231 au 3V3 (alimentation). Reliez chacun de leurs GND au GND (retour de courant). Reliez toutes les broches SDA à GPIO8 (données) et toutes les broches SCL à GPIO9 (horloge). Reliez RESET du MCP23017 au 3V3 (il reste actif).
- Ces modules ont des adresses différentes et peuvent partager les deux mêmes fils SDA et SCL.
- Montez le BMV080 derrière une ouverture protégée contre la pluie, la poussière directe et les insectes ; ne l’enfermez pas dans un compartiment étanche sans entrée d’air.
- Ne mettez jamais 5 V sur SDA ou SCL : les données utilisent 3,3 V et 5 V peut endommager la carte.
4. Câbler les signaux de feu, sol, vent et pluie
Reliez VCC du KY-026 et du capteur de sol au 3V3 (alimentation), et leurs GND au GND (retour de courant). Reliez AO du KY-026 à AIN0 de l’ADS1115 (signal de flamme) et AOUT du capteur de sol à AIN1 (signal de sol). Reliez une borne de l’anémomètre à GPA0 du MCP23017 (signal) et l’autre à GND ; reliez une borne du pluviomètre à GPA1 (signal) et l’autre à GND.
- Le programme compte les fermetures des deux petits interrupteurs internes pendant 20 secondes à chaque réveil.
- Éloignez le pluviomètre du panneau solaire et de la coupole afin qu’ils ne détournent pas la pluie.
- Ne reliez pas les fils du vent ou de la pluie à une alimentation externe : ce sont seulement des contacts secs.
5. Ajouter la mesure sûre de la batterie
Reliez le premier côté de la résistance supérieure 100 kΩ au +V de la 18650 (mesure). Reliez son autre côté à AIN2 de l’ADS1115. Reliez le premier côté de la seconde résistance 100 kΩ au même AIN2 (mesure), puis son autre côté au GND (retour de courant).
- Les deux résistances réduisent de moitié la tension de batterie avant la mesure ; le logiciel reconstitue la tension réelle.
- Le journal et les messages signalent une batterie faible avant une coupure complète.
- Ne branchez jamais le + de la 18650 directement sur une entrée de l’ESP32 ou de l’ADS1115 : une tension trop élevée peut détruire le module.
6. Câbler LoRa, microSD et alarmes
Reliez VCC de LoRa et de la microSD au 3V3 (alimentation), et GND au GND (retour de courant). Reliez MOSI à GPIO11 (données sortantes), MISO à GPIO13 (données entrantes) et SCK à GPIO12 (horloge) pour les deux modules. Reliez NSS LoRa à GPIO10 (sélection radio), DIO0 à GPIO40 (signal radio), CS microSD à GPIO41 (sélection mémoire) et RESET LoRa au 3V3. Reliez SIGNAL du buzzer à GPIO38 (alarme) et son GND à GND. Reliez GPIO39 à la résistance 220 Ω, puis à l’anode de la LED ; reliez la cathode de la LED à GND.
- Formatez la microSD en FAT32 avant le premier essai.
- Branchez l’antenne 433 MHz avant d’alimenter le module radio.
- Émettre avec LoRa sans antenne peut endommager ou affaiblir le module.
- GPIO41 est utilisé comme sélection de la microSD : vérifiez au sol que le fichier hazards.csv est créé.
7. Brancher le solaire et la batterie
Reliez V+ du panneau solaire 6 V / 4 W à VIN du chargeur solaire (entrée) et son GND à GND (retour). Reliez +V de la cellule 18650 à BAT (batterie), son GND à GND, puis utilisez seulement la sortie 3V3 du chargeur pour alimenter l’ESP32 et les capteurs.
- Testez d’abord avec l’USB de l’ESP32, puis raccordez le solaire et la batterie après avoir vérifié les tensions.
- Utilisez une cellule protégée et un support mécanique solide.
- Ne reliez jamais directement le panneau ou la cellule 18650 au 3V3 de l’ESP32 : une tension incorrecte peut détruire la carte.
- N’utilisez pas une cellule gonflée, chaude ou abîmée.
8. Tester et enregistrer la référence du site
Au sol, vérifiez que hazards.csv est créé, que la passerelle reçoit les trames LoRa et que LED et buzzer fonctionnent. Laissez ensuite la node mesurer plusieurs jours sans incident : elle apprend une température habituelle et marque les hausses anormales. Dans le logiciel du PC, enregistrez une fois l’identifiant forest-01, la position GPS réelle et la zone couverte du mât.
- Le PC doit considérer une alerte critique lorsqu’elle se répète ou lorsqu’une node voisine la confirme.
- Conservez les fichiers microSD : ils servent à calibrer les seuils et à mesurer le taux réel de fausses alertes.
- Cette station est un outil expérimental d’alerte précoce : une alerte grave doit toujours être confirmée par une seconde station, une image ou une équipe humaine.
Review all connections
1. Connections between "solar_panel" and "ESP32"
2. Connections between "battery_18650" and "ESP32"
3. Connections between "solar_power_manager" and "ESP32"
4. Connections between "bme280_env" and "ESP32"
5. Connections between "gy82_motion" and "ESP32"
6. Connections between "ads1115_adc" and "ESP32"
7. Connections between "flame_sensor" and "ESP32"
8. Connections between "soil_sensor" and "ESP32"
9. Connections between "lora_radio" and "ESP32"
10. Connections between "microsd_log" and "ESP32"
11. Connections between "alarm_buzzer" and "ESP32"
12. Connections between "led_resistor" and "ESP32"
13. Connections between "warning_led" and "ESP32"
14. Connections between "wind_anemometer" and "ESP32"
15. Connections between "rain_gauge" and "ESP32"
16. Connections between "fresnel_optical_dome" and "ESP32"
17. Connections between "weather_gpio_expander" and "ESP32"
18. Connections between "rtc_clock" and "ESP32"
19. Connections between "battery_divider_top" and "ESP32"
20. Connections between "battery_divider_bottom" and "ESP32"
Deploy the firmware
#include <Arduino.h>
#include <Wire.h>
#include <SPI.h>
#include <SD.h>
#include <LoRa.h>
#include <Adafruit_BME280.h>
#include <Adafruit_ADS1X15.h>
#include <Adafruit_MPU6050.h>
#include <Adafruit_Sensor.h>
#include <RTClib.h>
#include <esp_sleep.h>
constexpr char NODE_ID[] = "forest-01";
constexpr int I2C_SDA_PIN = 8, I2C_SCL_PIN = 9;
constexpr int LORA_NSS_PIN = 10, LORA_MOSI_PIN = 11, LORA_SCK_PIN = 12, LORA_MISO_PIN = 13, LORA_DIO0_PIN = 40;
constexpr int SD_CS_PIN = 41, BUZZER_PIN = 38, WARNING_LED_PIN = 39;
constexpr uint8_t MCP_ADDR = 0x20, WIND_INPUT = 0, RAIN_INPUT = 1;
constexpr long LORA_FREQUENCY_HZ = 433000000L;
constexpr uint64_t NORMAL_SLEEP_US = 300ULL * 1000000ULL, WATCH_SLEEP_US = 120ULL * 1000000ULL, ALERT_SLEEP_US = 60ULL * 1000000ULL;
constexpr float HOT_TEMPERATURE_C = 48.0f, WATCH_TEMPERATURE_C = 39.0f, DRY_HUMIDITY_PCT = 28.0f, IMPACT_G = 2.4f, PM_SMOKE_UG_M3 = 35.0f;
constexpr int16_t FLAME_CHANGE_COUNTS = 900;
constexpr float RAIN_MM_PER_TIP = 0.2794f, WIND_KMH_PER_HZ = 2.4f;
Adafruit_BME280 bme;
Adafruit_ADS1115 ads;
Adafruit_MPU6050 mpu;
RTC_DS3231 rtc;
RTC_DATA_ATTR uint32_t sequenceNumber = 0, rainTipsTotal = 0;
RTC_DATA_ATTR float previousTemperatureC = NAN, temperatureMean = 0.0f, temperatureVariance = 0.0f;
RTC_DATA_ATTR int16_t previousFlame = 0;
RTC_DATA_ATTR uint16_t baselineSamples = 0;
bool bmeReady, adsReady, mpuReady, weatherReady, rtcReady, sdReady, loraReady;
bool mcpWriteRegister(uint8_t reg, uint8_t value) {
Wire.beginTransmission(MCP_ADDR); Wire.write(reg); Wire.write(value);
return Wire.endTransmission() == 0;
}
bool mcpReadInput(uint8_t input) {
Wire.beginTransmission(MCP_ADDR); Wire.write(0x12);
if (Wire.endTransmission(false) != 0 || Wire.requestFrom((int)MCP_ADDR, 1) != 1) return true;
return (Wire.read() & (1U << input)) != 0;
}
int16_t readAdsAverage(uint8_t channel) {
if (!adsReady) return INT16_MIN;
int32_t sum = 0;
for (uint8_t i = 0; i < 8; ++i) { sum += ads.readADC_SingleEnded(channel); delay(4); }
return static_cast<int16_t>(sum / 8);
}
void setAlarm(bool on) { digitalWrite(WARNING_LED_PIN, on); ledcWriteTone(BUZZER_PIN, on ? 2200 : 0); }
void appendLog(const char *line) { if (sdReady) { File f = SD.open("/hazards.csv", FILE_WRITE); if (f) { f.println(line); f.close(); } } }
bool transmit(const char *line, uint8_t tries) {
if (!loraReady) return false;
digitalWrite(SD_CS_PIN, HIGH);
for (uint8_t i = 0; i < tries; ++i) { LoRa.idle(); LoRa.beginPacket(); LoRa.enableCrc(); LoRa.print(line); if (LoRa.endPacket() == 1) { LoRa.sleep(); return true; } delay(300); }
LoRa.sleep(); return false;
}
void sampleWeather(uint32_t &wind, uint32_t &rain) {
wind = rain = 0; if (!weatherReady) return;
bool oldWind = mcpReadInput(WIND_INPUT), oldRain = mcpReadInput(RAIN_INPUT);
uint32_t lastWind = 0, lastRain = 0, start = millis();
while (millis() - start < 20000UL) {
uint32_t now = millis(); bool w = mcpReadInput(WIND_INPUT), r = mcpReadInput(RAIN_INPUT);
if (oldWind && !w && now - lastWind > 35) { ++wind; lastWind = now; }
if (oldRain && !r && now - lastRain > 35) { ++rain; lastRain = now; }
oldWind = w; oldRain = r; delay(5);
}
}
float zScore(float t) { return baselineSamples >= 12 && temperatureVariance >= 0.04f ? (t - temperatureMean) / sqrtf(temperatureVariance) : 0.0f; }
void updateBaseline(float t, bool safe) {
if (!safe || isnan(t)) return;
++baselineSamples; float a = baselineSamples < 24 ? 1.0f / baselineSamples : 0.04f, d = t - temperatureMean;
temperatureMean += a * d; temperatureVariance = (1.0f - a) * (temperatureVariance + a * d * d);
}
void sleepFor(uint64_t us) { setAlarm(false); esp_sleep_enable_timer_wakeup(us); delay(20); esp_deep_sleep_start(); }
void setup() {
Serial.begin(115200); pinMode(WARNING_LED_PIN, OUTPUT); pinMode(SD_CS_PIN, OUTPUT); digitalWrite(SD_CS_PIN, HIGH); ledcAttach(BUZZER_PIN, 2200, 8);
Wire.begin(I2C_SDA_PIN, I2C_SCL_PIN);
bmeReady = bme.begin(0x77, &Wire) || bme.begin(0x76, &Wire);
adsReady = ads.begin(0x48, &Wire); if (adsReady) ads.setGain(GAIN_ONE);
mpuReady = mpu.begin(0x68, &Wire); if (mpuReady) { mpu.setAccelerometerRange(MPU6050_RANGE_4_G); mpu.setFilterBandwidth(MPU6050_BAND_21_HZ); }
Wire.beginTransmission(MCP_ADDR); weatherReady = (Wire.endTransmission() == 0);
if (weatherReady) { weatherReady = mcpWriteRegister(0x00, 0x03) && mcpWriteRegister(0x0C, 0x03); }
rtcReady = rtc.begin(&Wire); if (rtcReady && rtc.lostPower()) rtc.adjust(DateTime(__DATE__, __TIME__));
SPI.begin(LORA_SCK_PIN, LORA_MISO_PIN, LORA_MOSI_PIN, LORA_NSS_PIN); sdReady = SD.begin(SD_CS_PIN, SPI);
LoRa.setPins(LORA_NSS_PIN, -1, LORA_DIO0_PIN); loraReady = LoRa.begin(LORA_FREQUENCY_HZ); if (loraReady) { LoRa.setTxPower(14); LoRa.sleep(); }
float t = NAN, h = NAN, p = NAN; if (bmeReady) { t = bme.readTemperature(); h = bme.readHumidity(); p = bme.readPressure() / 100.0f; }
float g = NAN; if (mpuReady) { sensors_event_t a, gyro, imut; mpu.getEvent(&a, &gyro, &imut); g = sqrtf(a.acceleration.x*a.acceleration.x + a.acceleration.y*a.acceleration.y + a.acceleration.z*a.acceleration.z) / 9.80665f; }
int16_t flame = readAdsAverage(0), soil = readAdsAverage(1), batteryRaw = readAdsAverage(2);
float batteryV = batteryRaw == INT16_MIN ? NAN : batteryRaw * 0.000250f;
uint32_t windPulses, rainPulses; sampleWeather(windPulses, rainPulses); rainTipsTotal += rainPulses;
float windKmh = windPulses * WIND_KMH_PER_HZ / 20.0f, rainMm = rainTipsTotal * RAIN_MM_PER_TIP;
float rise = !isnan(previousTemperatureC) && !isnan(t) ? t - previousTemperatureC : 0.0f;
int32_t flameDelta = !isnan(previousTemperatureC) && flame != INT16_MIN ? abs((int32_t)flame - previousFlame) : 0;
bool hot = !isnan(t) && t >= HOT_TEMPERATURE_C, dryWarm = !isnan(t) && !isnan(h) && t >= WATCH_TEMPERATURE_C && h <= DRY_HUMIDITY_PCT;
bool rapidHeat = rise >= 3.0f, thermalAnomaly = zScore(t) >= 3.0f, optical = flameDelta >= FLAME_CHANGE_COUNTS, impact = !isnan(g) && g >= IMPACT_G, lowBattery = !isnan(batteryV) && batteryV < 3.35f;
uint8_t evidence = (hot || dryWarm) + (rapidHeat || thermalAnomaly) + optical;
bool fire = evidence >= 3, watch = evidence > 0 || impact || windKmh >= 45.0f || lowBattery;
updateBaseline(t, !fire && !impact); previousTemperatureC = t; if (flame != INT16_MIN) previousFlame = flame; ++sequenceNumber;
uint32_t epoch = rtcReady ? rtc.now().unixtime() : 0;
char msg[280]; snprintf(msg, sizeof(msg), "id=%s,seq=%lu,ts=%lu,t=%.1f,h=%.1f,p=%.1f,fl=%d,soil=%d,bat=%.3f,wind=%.1f,rain=%.2f,g=%.2f,z=%.2f,r=%u,ev=%u,fire=%u,impact=%u,lowbat=%u,cam=event_pending,ok=%u%u%u%u", NODE_ID, (unsigned long)sequenceNumber, (unsigned long)epoch, t,h,p,flame,soil,batteryV,windKmh,rainMm,g,zScore(t),fire?3:(watch?1:0),evidence,fire,impact,lowBattery,bmeReady,adsReady,mpuReady,rtcReady);
appendLog(msg); bool sent = transmit(msg, fire ? 3 : 1); Serial.printf("%s,tx=%u\n", msg, sent);
if (fire || impact) { uint32_t start = millis(); while (millis() - start < 12000UL) { setAlarm(true); delay(220); setAlarm(false); delay(220); } sleepFor(ALERT_SLEEP_US); }
sleepFor(watch ? WATCH_SLEEP_US : NORMAL_SLEEP_US);
}
void loop() {}Remix this project
Make it yours in one click
Open a full copy of this project in your own Schematik workspace — diagram, code, parts, and assembly steps included. Swap the sensor, add features, or redesign the whole thing with AI. The author's original stays untouched.




