Siddhant Kumar
Project 042 · Environment

Forest Fire Early Detector.

A mesh of self-powered wireless nodes that smells and feels a wildfire in its first minutes — when a satellite still sees nothing and a lookout is still hours from noticing.

Advanced 14–22 hours 37 min read SensorsLoRaSafety
Jump to source Bill of materials
Forest Fire Early Detector — reference build illustration MCU VCC · GND · SIG · NC
Difficulty
Advanced
Build time
14–22 hours
Indicative cost
₹4,200 – ₹5,800 per node
Platform
ESP32 DevKit V1 (ESP-WROOM-32)
Category
Environment
Last updated
28 July 2026
Contents — 26 sections

Project Overview

A mesh of self-powered wireless nodes that smells and feels a wildfire in its first minutes — when a satellite still sees nothing and a lookout is still hours from noticing.

A wildfire is almost survivable in its first ten minutes and almost unstoppable an hour later. The whole game is early detection, and the tools we usually rely on are late: satellites revisit on a schedule and need a fire big and hot enough to show through cloud and canopy; watchtowers and cameras see smoke only once a plume has risen above the trees; a 112/emergency call needs a human who has already noticed. Down in the understory, though, a fire announces itself long before any of that — a sharp rise in temperature, a collapse in local humidity, and above all the smoke and combustion gases that pour off smouldering vegetation minutes before there are visible flames. This project puts cheap sensors down where the fire starts and networks them so that first chemical whisper becomes an alert.

Each node is a small, rugged, solar-powered box that samples the air for the signature of combustion — smoke particulate and carbon-monoxide-rich gases from a metal-oxide sensor — together with temperature and humidity, and watches for the combination that means fire rather than any single cue. That combination matters enormously: a hot afternoon is not a fire, a dust cloud is not a fire, but rising smoke gas AND rising temperature AND falling humidity together, appearing suddenly, is. By fusing the channels and looking at rate-of-change against each node's own learned baseline, the detector fires on real events and stays quiet through the daily weather, which is the difference between a system people trust and one they mute.

The other half of the design is the network. A single node covers a small patch, so detection at landscape scale means many nodes spread across a forest — and forests have no mains power and no Wi-Fi. The nodes therefore run on solar and talk over LoRa in a mesh, each relaying its neighbours' messages so an alert from deep in the trees hops node-to-node out to a gateway at the forest edge and onward to the fire service, with the node's location. It is candid about its limits — low-cost gas sensors are indicative, coverage depends on node density, and it complements rather than replaces satellites and lookouts — but a dense mesh of honest, fast, ground-level nodes buys the one thing wildfire response never has enough of: minutes.

A photovoltaic solar panel in sunlight
Solar power lets fire nodes live for seasons deep in forest where there is no mains and no cellular. Photograph sourced from Wikimedia Commons — Solar panel.jpg. Reused under the licence stated on that page; please check it before republishing.

What this project does

  • Samples smoke/combustion gas, temperature and humidity at ground level
  • Fuses the channels and fires on the combination that means fire, not single cues
  • Detects by rate-of-change against each node's learned baseline to reject weather
  • Relays alerts node-to-node over a LoRa mesh to a forest-edge gateway
  • Reports each alert with the originating node's location
  • Runs unattended for seasons on solar + battery
  • Escalates a confirmed detection immediately to responders

Real-World Applications

SettingHow it is used
High-risk forest and wildland-urban interfaceDense node coverage over fire-prone forest or the vulnerable edge where settlements meet wildland, buying response time for the highest-consequence areas.
Plantations and managed forestryProtecting commercial timber and preventing a small ignition from destroying years of growth.
Protected areas and biodiversity reservesEarly alerts in remote reserves where no lookout exists and access is slow.
Peatland and agricultural-burn monitoringCatching smouldering peat or escaped stubble fires that produce heavy smoke gas before open flame.

Deployment contexts where a build of this kind earns its keep.

Features & Capabilities

  • Ground-level chemical detection — minutes before smoke rises or satellites see
  • Multi-cue fusion (smoke + heat + humidity drop) to reject false alarms
  • Per-node adaptive baselines so normal weather does not trigger it
  • LoRa mesh relaying for coverage deep in roadless, powerless forest
  • Solar, rugged, season-long unattended operation
  • Located alerts routed straight to the fire service
  • Honest about coverage and sensor limits — a complement to satellites/lookouts

Difficulty, Time & Required Skills

AttributeValue
Difficulty levelAdvanced
Estimated completion time14–22 hours
Indicative build cost₹4,200 – ₹5,800 per node
Primary disciplineEnvironment
Reference platformESP32 DevKit V1 (ESP-WROOM-32)

Skills you should have (or will pick up)

  • Reading smoke/gas (metal-oxide) sensors and interpreting them qualitatively
  • Multi-sensor fusion and rate-of-change (baseline) detection
  • Building a LoRa mesh with message relaying and deduplication
  • Solar power design for season-long remote nodes
  • Ruggedising electronics for outdoor, high-temperature environments

Bill of Materials

Every part below is commonly available from Indian and international hobby-electronics suppliers. Prices are indicative 2026 retail figures in Indian rupees and will drift — treat them as a budgeting guide, not a quotation.

ComponentKey specificationQtyApprox. cost
ESP32 DevKit V1 (ESP-WROOM-32)
Wi-Fi transmit bursts peak near 500 mA — size the regulator accordingly.
Dual-core Xtensa LX6 @ 240 MHz, 520 KB SRAM, 4 MB flash, Wi-Fi 802.11 b/g/n + BLE 4.2, 34 GPIO, 18× 12-bit ADC, 2× 8-bit DAC1₹450
MQ-2 combustible gas / smoke sensor
Needs 24–48 h burn-in and a stable 5 V; the heater alone draws ~150 mA.
300–10000 ppm LPG, propane, methane, hydrogen, smoke; analogue + digital output1₹150
MQ-135 air-quality sensor
Not a true CO₂ sensor — calibrate against clean air (R0) before trusting ppm.
NH₃, NOx, benzene, smoke, CO₂ proxy, 10–1000 ppm, analogue output1₹180
DHT22 / AM2302 temperature + humidity sensor
Needs a 4.7 kΩ pull-up on the data line and 2 s between reads.
−40 to +80 °C ±0.5 °C, 0–100 %RH ±2 %, 0.5 Hz sample rate, single-wire digital1₹250
BME280 pressure/humidity/temperature sensor
Self-heating skews temperature by ~1 °C — read in forced mode, not continuous.
300–1100 hPa ±1 hPa, 0–100 %RH ±3 %, −40 to +85 °C ±1 °C, 3.4 µA at 1 Hz1₹420
IR flame sensor module (YG1006)
Sunlight and incandescent bulbs both trigger it — always confirm with a second sensor type.
760–1100 nm, 60° detection cone, 0.8 m range, analogue + digital out1₹70
SX1278 LoRa 433 MHz module (Ra-02)
Never power the radio without an antenna — the PA will destroy itself.
−148 dBm sensitivity, +20 dBm output, up to 10 km line of sight, SF7–SF121₹480
20 W 12 V polycrystalline solar panel
Rated watts assume 1000 W/m² — plan for 60–70 % of nameplate in real installs.
Vmp 17.5 V, Imp 1.14 A, Voc 21.6 V, 350 × 290 mm, aluminium frame1₹1,200
TP4056 Li-ion charger + DW01 protection
Buy the version *with* protection ICs — the bare charger will over-discharge your cell.
1 A programmable CC/CV charge to 4.2 V ±1 %, over-discharge and short protection1₹45
18650 Li-ion cell 3400 mAh + holder
Never charge below 0 °C; always use a protected cell or a BMS.
3.7 V nominal, 4.2 V full, 3400 mAh, ~12.6 Wh, 2 C discharge1₹450
Rugged UV/heat-resistant enclosure
Must let air reach the gas sensor but shelter the board
Vented for gas ingress, IP-rated against rain/dust, shades the electronics1₹550
High-temperature battery + protection
Standard Li-ion degrades/fails in forest summer heat
LiFePO4 preferred for heat tolerance, with over-temperature cutoff1₹700
LoRa mesh gateway (edge)
Shared across the whole mesh
One gateway at the forest edge with backhaul (cellular/Ethernet) to responders1₹2,500

Estimated total: ₹7,445, excluding tools, shipping and consumables.

Tools and consumables

  • Soldering iron (temperature controlled, 350 °C) with 0.8 mm 60/40 or lead-free solder
  • Digital multimeter — continuity, DC volts and current ranges
  • Wire strippers, flush cutters and a small set of precision screwdrivers
  • Heat-shrink tubing and a heat gun (or a lighter, carefully)
  • A laptop with a USB port and the toolchain listed above

Hardware Specifications

PartSpecificationSupplyInterfaceReference
ESP32 DevKit V1 (ESP-WROOM-32)Dual-core Xtensa LX6 @ 240 MHz, 520 KB SRAM, 4 MB flash, Wi-Fi 802.11 b/g/n + BLE 4.2, 34 GPIO, 18× 12-bit ADC, 2× 8-bit DAC3.3 V logic / 5 V USBUART, SPI, I²C, I²S, CAN, PWMDatasheet
MQ-2 combustible gas / smoke sensor300–10000 ppm LPG, propane, methane, hydrogen, smoke; analogue + digital output5 V (heater)Analogue + comparator digitalDatasheet
MQ-135 air-quality sensorNH₃, NOx, benzene, smoke, CO₂ proxy, 10–1000 ppm, analogue output5 V (heater)AnalogueDatasheet
DHT22 / AM2302 temperature + humidity sensor−40 to +80 °C ±0.5 °C, 0–100 %RH ±2 %, 0.5 Hz sample rate, single-wire digital3.3–6 V1-wire proprietaryDatasheet
BME280 pressure/humidity/temperature sensor300–1100 hPa ±1 hPa, 0–100 %RH ±3 %, −40 to +85 °C ±1 °C, 3.4 µA at 1 Hz1.7–3.6 V (module has 3.3 V LDO)I²C (0x76/0x77) or SPIDatasheet
IR flame sensor module (YG1006)760–1100 nm, 60° detection cone, 0.8 m range, analogue + digital out3.3–5 VAnalogue + digitalDatasheet
SX1278 LoRa 433 MHz module (Ra-02)−148 dBm sensitivity, +20 dBm output, up to 10 km line of sight, SF7–SF123.3 VSPIDatasheet
20 W 12 V polycrystalline solar panelVmp 17.5 V, Imp 1.14 A, Voc 21.6 V, 350 × 290 mm, aluminium frame12 V nominalMC4 / screw terminalsDatasheet
TP4056 Li-ion charger + DW01 protection1 A programmable CC/CV charge to 4.2 V ±1 %, over-discharge and short protection4.5–5.5 V inmicro-USB / padsDatasheet
18650 Li-ion cell 3400 mAh + holder3.7 V nominal, 4.2 V full, 3400 mAh, ~12.6 Wh, 2 C discharge3.0–4.2 VHolder / spot-welded tabsDatasheet

Consolidated electrical and interface specifications for every active part in the build.

Power Budget & Supply Sizing

Add up the typical active current of every part, then size the supply with at least 50 % headroom so transmit bursts and motor inrush never brown out the controller.

LoadSupply railTypical current (mA)Notes
ESP32 DevKit V1 (ESP-WROOM-32)3.3 V logic / 5 V USB160Wi-Fi transmit bursts peak near 500 mA — size the regulator accordingly.
MQ-2 combustible gas / smoke sensor5 V (heater)150Needs 24–48 h burn-in and a stable 5 V; the heater alone draws ~150 mA.
MQ-135 air-quality sensor5 V (heater)150Not a true CO₂ sensor — calibrate against clean air (R0) before trusting ppm.
DHT22 / AM2302 temperature + humidity sensor3.3–6 V1.5Needs a 4.7 kΩ pull-up on the data line and 2 s between reads.
BME280 pressure/humidity/temperature sensor1.7–3.6 V (module has 3.3 V LDO)0.4Self-heating skews temperature by ~1 °C — read in forced mode, not continuous.
IR flame sensor module (YG1006)3.3–5 V15Sunlight and incandescent bulbs both trigger it — always confirm with a second sensor type.
SX1278 LoRa 433 MHz module (Ra-02)3.3 V120Never power the radio without an antenna — the PA will destroy itself.
20 W 12 V polycrystalline solar panel12 V nominal1140Rated watts assume 1000 W/m² — plan for 60–70 % of nameplate in real installs.
TP4056 Li-ion charger + DW01 protection4.5–5.5 V in1000Buy the version *with* protection ICs — the bare charger will over-discharge your cell.

Summed typical draw is 2736.9 mA. With a 1.5× design margin the supply should deliver at least 4200 mA continuously at the stated rail voltage.

Software Requirements & Development Environment

Reference toolchain: Arduino IDE 2.3.x with the ESP32 board package 3.x (or PlatformIO on VS Code). Anything newer normally works; anything older may lack the board definitions used here.

  • Install the Arduino IDE 2.3.x (or PlatformIO if you prefer a real editor and dependency locking).
  • Add https://espressif.github.io/arduino-esp32/package_esp32_index.json under File → Preferences → Additional Board Manager URLs, then install esp32 from the Boards Manager.
  • Set the correct port under Tools → Port. On Linux add yourself to the dialout group: sudo usermod -aG dialout $USER and log out and back in.
  • Open the Serial Monitor at 115200 baud — every sketch here logs its state there.
  • Keep File → Preferences → Show verbose output during: compilation switched on while you are debugging build errors.

Required libraries

LibraryWhy it is neededInstall
WiFi (ESP32 core) bundledStation/AP connection management for the ESP32.Bundled with the ESP32 Arduino core
DHT sensor library 1.4.6Timing-critical driver for DHT11/DHT22.Library Manager → "DHT sensor library" by Adafruit
Adafruit BME280 2.2.xCompensation maths for the Bosch pressure/humidity/temperature sensor.Library Manager → "Adafruit BME280 Library"
LoRa (sandeepmistry) 0.8.0SX127x radio configuration, packet TX/RX and callbacks.Library Manager → "LoRa" by Sandeep Mistry
ArduinoJson 7.xZero-allocation JSON serialisation and parsing.Library Manager → "ArduinoJson" by Benoit Blanchon
Preferences (NVS) bundledWear-levelled key/value storage in ESP32 flash for settings.Bundled with the ESP32 core

Block Diagram

The block diagram shows the functional decomposition of the system — what senses, what decides, what acts, and where the data ends up.

Forest Fire Early Detector — system block diagramFunctional block diagram of the Forest Fire Early Detector system. Smell + feelSmoke/gasMQ-2 / MQ-135Temp + RHDHT22 / BME280Flame (confirm)IR sensorFuse + decideESP32multi-cue + baselineFire scorecombination ruleMeshLoRa relaynode → nodeEdge gatewayto respondersResponseFire servicelocated alertDashboardnode health maprightrightnone
Forest Fire Early Detector — system block diagram

Circuit Diagram & Wiring

Every signal line in the build is shown below, followed by a pin-by-pin connection table you can work through with a multimeter in hand.

Forest Fire Early Detector — wiring schematicConnection schematic showing which controller pin drives each peripheral. Sensors / InputsControllerActuators / OutputsESP32 DevKit V1(ESP-WROOM-32)3.3 V logic / 5 V USBMQ-2 (smoke)GPIO 34 (ADC)Smoke/combustiblegasMQ-135 (gas)GPIO 35 (ADC)CO/VOC combustiongasesDHT22 / BME280GPIO 4 / 21-22Temp + humidityFlame sensorGPIO 27IR flame(line-of-sightconfirm)LoRa SX1276GPIO 18/19/23SPI mesh radioLoRa SX1276GPIO 5/14/2Chip-select, reset,IRQTP4056VIN / 3V3 regSolar-charged supplySolar panelTP4056 IN6 V panel → charger
Forest Fire Early Detector — wiring schematic
PeripheralPeripheral pinController pinSignal
MQ-2 (smoke)AOUTGPIO 34 (ADC)Smoke/combustible gas
MQ-135 (gas)AOUTGPIO 35 (ADC)CO/VOC combustion gases
DHT22 / BME280DATA / I²CGPIO 4 / 21-22Temp + humidity
Flame sensorDOUTGPIO 27IR flame (line-of-sight confirm)
LoRa SX1276SCK/MISO/MOSIGPIO 18/19/23SPI mesh radio
LoRa SX1276NSS/RST/DIO0GPIO 5/14/2Chip-select, reset, IRQ
TP4056OUTVIN / 3V3 regSolar-charged supply
Solar panel+/–TP4056 IN6 V panel → charger

Wire one row at a time and tick it off — most "it does not work" reports trace back to a single swapped pair.

Wiring explanation

  • Vent the enclosure so outside air reaches the gas sensors, but shade the electronics and battery from direct sun and the worst heat.
  • Metal-oxide gas sensors have a heated element that draws steady current and needs warm-up; budget for it and keep its heater noise off the analogue grounds.
  • Place the temperature/humidity sensor in the same vented airflow so its readings represent the air the gas sensors sample.
  • Mount the flame sensor with a clear line of sight where possible; treat it as confirmation, not primary detection, since it needs direct view of flame.
  • Keep the LoRa antenna vertical and as high as the mounting allows for mesh range under canopy.
An ESP32 development board with the ESP-WROOM-32 module and USB connector
ESP32 module fusing smoke, heat and humidity cues against a learned baseline to confirm fire. Photograph sourced from Wikimedia Commons — ESP32 Espressif ESP-WROOM-32 Dev Board.jpg. Reused under the licence stated on that page; please check it before republishing.

System Architecture

Read the stack from the bottom up: physical hardware, the firmware that drives it, the transport that moves data off the device, and the software a human actually looks at.

Forest Fire Early Detector — architecture stackLayered architecture from hardware to user interface. Hardware layerESP32 DevKit V1 (ESP-WROOM-32) · MQ-2 combustible gas / smoke sensor ·MQ-135 air-quality sensor · DHT22 / AM2302 temperature + humidity sensorDriver layerwifi · dhtlib · bme · lorolibApplication logicsampling loop · filtering · thresholds · state machineTransport layerLoRa mesh → edge gateway → fire service · TLS · retry and backoffPresentation layerdashboard · mobile notifications · historical charts
Forest Fire Early Detector — architecture stack

Working Principle

The detector wins on physics of timing. A ground fire begins as smouldering combustion in leaf litter and undergrowth, and long before there are flames tall enough to see or hot enough for a satellite's thermal band, it is emitting a plume of smoke particulate and combustion gases — carbon monoxide and a soup of volatile organics — right at the height where our nodes sit. It also locally spikes the temperature and drives down the relative humidity as it heats and dries the air around it. Detecting these near the source, in the first minutes, is inherently earlier than any method that waits for the fire to grow large enough to be visible from above or from a distance.

The central design principle is multi-cue fusion, because no single sensor can tell fire from ordinary environmental variation. A metal-oxide gas sensor rises on a fire — but also on a passing vehicle's exhaust, a nearby cooking fire, or its own drift. Temperature rises on a fire — but also every sunny afternoon. Humidity falls in a fire — but also in a dry wind. Each cue alone would either false-alarm constantly or miss real fires depending on where you set the threshold. Fused, they become specific: it is the coincidence of rising combustion gas, rising temperature and falling humidity, arriving together and quickly, that is the fingerprint of fire and almost nothing else. The node computes a combined fire score from all channels rather than trusting any one.

Because the cheap gas sensors drift and every site has a different "normal", detection is done by rate-of-change against a per-node adaptive baseline, not fixed thresholds. Each node continuously learns its own slow background for each channel; an alarm needs a departure that is fast and large relative to that baseline and the channel's normal noise. This makes a node in a humid valley and one on a dry ridge each judge fire by its own normal, absorbs slow sensor drift automatically, and keys on the sudden coincident change that fire produces rather than any absolute value. A flame sensor, where it has line of sight, adds a final confirmation channel — direct evidence of flame that sharply raises confidence when present, though its short range and need for a clear view keep it a confirmer rather than the primary detector.

Detection at landscape scale is a networking problem as much as a sensing one. One node protects a small radius, so real coverage means a dense field of nodes — and forests are exactly where there is no power and no cellular. The answer is solar nodes on a LoRa mesh: each node not only sends its own messages but relays its neighbours', so an alert originating deep in roadless terrain hops from node to node until it reaches a gateway at the forest edge with backhaul to the fire service. Messages carry the originating node's ID and location and a hop count, and the mesh deduplicates and rate-limits relays so one alert does not storm the network. The system is deliberately honest about the trade-offs — coverage is only as good as node density, and a low-cost gas sensor is indicative not analytical — but as a fast, ground-truth complement to satellites and lookouts, a mesh like this delivers the minutes that decide whether a fire is a footnote or a catastrophe.

The maths behind it

Per-node adaptive baseline and anomaly

plainPer-node adaptive baseline and anomaly
For each channel x (smoke, gas, temp, −RH):

  base ← base + α·(x − base)         (α small, slow)
  var  ← 0.98·var + 0.02·(x − base)^2
  z_x  = (x − base) / (sqrt(var) + ε)

z_x is how many "normals" this channel has jumped.
Using −RH means a humidity DROP contributes positively,
aligning all cues so a fire pushes every z_x upward.

Fused fire score

plainFused fire score
Combine the standardised cues (require coincidence):

  score = w1·z_smoke + w2·z_gas + w3·z_temp + w4·z_negRH
          + FLAME_BONUS·flame_confirmed

Alarm if score > S_thresh sustained over N reads.
Weights w emphasise the gas/smoke channels; the flame
bonus sharply raises confidence when a flame is seen.
Coincidence (several z high together) is what fire looks
like — one channel alone rarely crosses S_thresh.

Mesh relay with deduplication

plainMesh relay with deduplication
Each alert packet: {node_id, lat, lon, score, msg_id, hops}

  on receive:
    if msg_id already seen  → drop (dedup)
    else record msg_id; if hops < HOP_MAX:
         hops++ ; rebroadcast after random backoff

Random backoff avoids collisions; HOP_MAX bounds flooding;
dedup stops a message circulating forever. The alert walks
outward to the edge gateway node by node.

Program Flowchart

The firmware is a single cooperative loop. Nothing blocks for long, so networking, sensing and the user interface all stay responsive.

Forest Fire Early Detector — firmware flowchartControl flow through the main program loop. Wake on scheduleRead smoke, gas, temp, RHUpdate per-node baselinesSmoke↑ AND temp↑ AND RH↓together?Raise fire scoreLog; sleepRaise fire scoreScore over threshold(sustained)?Emit located alert into meshLog; sleepEmit located alert into meshLog; sleep
Forest Fire Early Detector — firmware flowchart

Assembly Instructions

Build on a breadboard first and only commit to solder once the whole system has run for an hour without a fault.

  1. Build the vented, heat-tolerant node

    House the electronics in a rugged, UV-resistant enclosure that is vented so outside air reaches the gas sensors while the board and battery are shaded from direct sun.

    Use a heat-tolerant battery (LiFePO4) with over-temperature protection — forest summer heat destroys ordinary Li-ion.

  2. Fit and warm the sensors

    Mount the MQ-2/MQ-135 in the vented airflow and allow their heaters to stabilise (a warm-up period) before trusting readings. Place the temperature/humidity sensor in the same airflow, and the flame sensor with a clear view where possible.

  3. Set up solar and the mesh radio

    Angle the solar panel to the sun; size the panel and battery for the shortest winter days and canopy shade. Mount the LoRa antenna high and vertical for mesh range, and place gateways at the forest edge with backhaul.

Step-by-Step Implementation Guide

Work through these in order. Each step ends in something you can observe, so a failure is always localised to the step you just finished.

  1. Learn baselines and standardise cues

    Let each node learn a slow baseline and variance per channel, and standardise each reading into a z-score of how far it has jumped from that node's own normal.

  2. Fuse into a fire score with coincidence

    Combine the standardised cues into a single score that only crosses threshold when several channels rise together, and require the score to persist to reject transient spikes.

    cppfire-fusion.ino
    struct Chan { float base, var; bool primed; };
    Chan smoke, gas, temp, humid;
    
    float zscore(Chan &c, float x) {
      if (!c.primed) { c.base = x; c.var = 1; c.primed = true; return 0; }
      float d = x - c.base;
      c.var  = 0.98f * c.var + 0.02f * d * d;
      c.base += 0.02f * d;                       // slow per-node baseline
      return d / (sqrtf(c.var) + 1e-3f);
    }
    
    // Fuse cues; humidity is entered as its NEGATIVE so a drop reads positive.
    float fireScore(float smk, float g, float t, float rh, bool flame) {
      float zs = zscore(smoke, smk);
      float zg = zscore(gas,   g);
      float zt = zscore(temp,  t);
      float zh = zscore(humid, -rh);            // humidity DROP → positive z
      float score = 1.2f*zs + 1.2f*zg + 0.9f*zt + 0.9f*zh;
      if (flame) score += 3.0f;                 // direct flame confirmation
      return score;
    }
    
    // Alarm needs coincidence AND persistence, not one loud channel.
    bool fireConfirmed(float score, uint8_t &nHigh) {
      if (score > 5.0f) nHigh++; else nHigh = 0;
      return nHigh >= 3;                         // sustained multi-cue rise
    }
    return d / (sqrtf(c.var) + 1e-3f)Each channel is expressed as how many of its own normal fluctuations it has jumped, so drift and site differences wash out and only genuine departures count.
    float zh = zscore(humid, -rh)Feeding negative humidity makes a humidity drop contribute positively, aligning all four cues so a real fire pushes every one of them up together.
    if (flame) score += 3.0fA line-of-sight flame detection adds a large bonus — direct evidence of fire that sharply raises confidence when available.
    return nHigh >= 3Confirmation requires the fused score to stay high for several reads, so a single sensor glitch or a passing exhaust puff cannot trip a landscape-scale alert.
  3. Alert into the mesh with location

    On a confirmed detection, emit a located alert packet into the LoRa mesh with a unique message id; relay neighbours' alerts with deduplication and a hop limit so every alert reaches the edge gateway once.

Complete Source Code

The listing below is complete and compiles as written — there are no elided sections. Read the annotations under each block before you upload it.

cppforest-fire-node.ino
/* ═══════════════════════════════════════════════════════════════
   Forest Fire Early Detector — ESP32, smoke/gas/temp/RH, LoRa mesh

   Fuses ground-level combustion cues against per-node baselines,
   confirms fire by coincidence + persistence, and relays located
   alerts through a solar LoRa mesh to a forest-edge gateway.
   ══════════════════════════════════════════════════════════════════ */

#include <DHT.h>
#include <LoRa.h>
#include <SPI.h>
#include <Preferences.h>
#include <math.h>

#define PIN_SMOKE 34
#define PIN_GAS   35
#define PIN_DHT    4
#define PIN_FLAME 27
#define LORA_CS    5
#define LORA_RST  14
#define LORA_DIO0  2
#define HOP_MAX    6
#define SLEEP_S   60          // 1 min — fast enough for early detection

DHT dht(PIN_DHT, DHT22);
Preferences prefs;

const uint16_t NODE_ID = 1;
float NODE_LAT, NODE_LON;

RTC_DATA_ATTR struct C { float base, var; bool primed; }
  cSmoke, cGas, cTemp, cHumid;
RTC_DATA_ATTR uint32_t msgCounter = 0;
RTC_DATA_ATTR uint32_t seenIds[16]; RTC_DATA_ATTR uint8_t seenN = 0;

float z(struct C &c, float x) {
  if (!c.primed) { c.base = x; c.var = 1; c.primed = true; return 0; }
  float d = x - c.base;
  c.var  = 0.98f * c.var + 0.02f * d * d;
  c.base += 0.02f * d;
  return d / (sqrtf(c.var) + 1e-3f);
}

bool seen(uint32_t id) {
  for (int i = 0; i < seenN; i++) if (seenIds[i] == id) return true;
  seenIds[seenN % 16] = id; seenN++;
  return false;
}

void sendAlert(float score) {
  uint32_t id = ((uint32_t)NODE_ID << 16) | (++msgCounter & 0xFFFF);
  char pkt[160];
  snprintf(pkt, sizeof pkt,
    "{\"t\":\"fire\",\"node\":%u,\"lat\":%.5f,\"lon\":%.5f,"
    "\"score\":%.1f,\"id\":%lu,\"hops\":0}",
    NODE_ID, NODE_LAT, NODE_LON, score, (unsigned long)id);
  LoRa.beginPacket(); LoRa.print(pkt); LoRa.endPacket();
}

// Relay any alert we hear (dedup + hop limit) so it walks to the edge.
void relayIfNeeded() {
  int sz = LoRa.parsePacket();
  if (!sz) return;
  char buf[200]; int n = 0;
  while (LoRa.available() && n < 199) buf[n++] = LoRa.read();
  buf[n] = 0;
  // (a real build parses JSON; shown conceptually)
  uint32_t id; int hops;
  if (parseAlert(buf, id, hops)) {
    if (seen(id)) return;                    // dedup
    if (hops < HOP_MAX) {
      delay(random(20, 200));                // random backoff vs collisions
      char out[210]; bumpHops(buf, out);     // hops+1
      LoRa.beginPacket(); LoRa.print(out); LoRa.endPacket();
    }
  }
}

void setup() {
  Serial.begin(115200);
  pinMode(PIN_FLAME, INPUT);
  dht.begin();
  prefs.begin("fire", true);
  NODE_LAT = prefs.getFloat("lat", 0); NODE_LON = prefs.getFloat("lon", 0);
  prefs.end();

  analogSetPinAttenuation(PIN_SMOKE, ADC_11db);
  analogSetPinAttenuation(PIN_GAS,   ADC_11db);

  SPI.begin();
  LoRa.setPins(LORA_CS, LORA_RST, LORA_DIO0);
  LoRa.begin(433E6);
  LoRa.setSpreadingFactor(10);

  // relay anything heard while we were asleep/awake
  relayIfNeeded();

  // ── read + fuse this node's cues ──
  float smk = analogRead(PIN_SMOKE);
  float g   = analogRead(PIN_GAS);
  float t   = dht.readTemperature();
  float rh  = dht.readHumidity();
  bool flame = digitalRead(PIN_FLAME) == LOW;

  float score = 1.2f*z(cSmoke, smk) + 1.2f*z(cGas, g)
              + 0.9f*z(cTemp, t)    + 0.9f*z(cHumid, -rh)
              + (flame ? 3.0f : 0.0f);

  static uint8_t nHigh;
  if (score > 5.0f) nHigh++; else nHigh = 0;
  if (nHigh >= 3) sendAlert(score);          // confirmed: coincidence+persist

  esp_sleep_enable_timer_wakeup((uint64_t)SLEEP_S * 1000000ULL);
  esp_deep_sleep_start();
}

void loop() {}   // deep sleep restarts setup()
RTC_DATA_ATTR struct CEach channel's baseline and variance persist across deep sleep in RTC memory, so a node keeps its learned sense of "normal" between one-minute wakes instead of re-learning and false-alarming.
bool seen(uint32_t id)Remembers recently-relayed message ids so the same alert is not rebroadcast twice — the deduplication that keeps one detection from storming the mesh.
void relayIfNeeded()Every node forwards its neighbours' alerts, with a random backoff and a hop limit, so a message from deep in the forest hops outward to the edge gateway without collisions or endless circulation.
float score = 1.2f*z(cSmokeThe fused fire score weights the smoke and gas channels most, adds temperature and humidity-drop, and a flame bonus — only their coincidence crosses threshold.
if (nHigh >= 3) sendAlert(score)An alert is emitted only when a multi-cue rise persists for several reads, the combination that distinguishes a real fire from weather or a sensor glitch.

Configuration & Calibration

Configuration steps

  • Set each node's location (lat/lon) in flash so alerts are geolocated.
  • Tune the cue weights, the score threshold and the persistence count from field trials in your vegetation type.
  • Set the sampling interval (1 min for fast detection) and the mesh HOP_MAX to your network diameter.
  • Choose the region-legal LoRa frequency and place edge gateways with reliable backhaul to responders.

Calibration procedure

An uncalibrated sensor produces confident, precise, wrong numbers. Do this once per physical unit and record the constants.

  1. Baseline settling

    At deployment, let baselines learn for hours to a day (sensors warmed up, weather sampled) before enabling alerts so a node does not trip on its own start-up.

  2. Controlled smoke test

    With authorisation and safety, introduce a small controlled smoke source near a node and confirm the fused score rises and confirms while single-channel noise does not.

  3. Mesh range and relay

    Verify neighbour-to-neighbour range under canopy and that a test alert from the deepest node reaches the edge gateway with sensible hop counts.

Network Architecture & Connectivity

Forest Fire Early Detector — network topologyPath taken by telemetry from field node to end user. Edge nodesGatewayCloudClientsFire nodeESP32 + gasRelay nodesmesh peersLoRa meshEdge gatewaycellular/Ethernet backhaulMQTT/HTTPSAlert serverdispatch + mapFire servicelocated alertsDashboardnode health
Forest Fire Early Detector — network topology

Communication protocol

Nodes sample every minute and stay silent unless a fire is confirmed; alerts are small located packets that flood outward through the mesh with deduplication and a hop limit, reaching the edge gateway and then responders in seconds.

Topic / endpointDirectionPayload
fire/mesh/alertnode → gatewaylocated fire alert (node, lat/lon, score, id, hops)
fire/node/healthnode → gatewaybattery, baselines, RSSI (periodic)
fire/gateway/dispatchgateway → respondersgeolocated dispatch with confidence

Message contract between the device and the broker.

Cloud platform configuration

An alert server geolocates each detection, correlates nearby nodes (several nodes confirming raises confidence), and dispatches to the fire service with a map, while a health map tracks battery and connectivity of every node.

Dashboard setup

A forest map of node health with instant red alerting, showing the originating node, fused score, relay path and any corroborating neighbours.

Mobile app integration

Immediate located push/SMS to responders on a confirmed detection, with confidence raised when multiple nodes agree.

Security considerations

  • Authenticate and sign alerts so a false fire cannot be injected to waste response resources.
  • Rate-limit and dedup relays so the mesh cannot be flooded, accidentally or maliciously.
  • Monitor node health so a cluster going silent (possibly burned or failed) is itself a signal.

Testing Procedure & Expected Output

Test from the bottom up. Confirm power, then each sensor in isolation, then the integrated loop — the first failing step tells you exactly where to look.

TestWhat you should see
Heat one channel only (e.g. warm the node)Score stays below threshold — single cue is not fire
Introduce smoke + heat + dry air togetherFused score rises, persists, and an alert is emitted
Trigger the flame sensor with a safe flame in viewScore jumps via the flame bonus; confirmation faster
Inject a test alert at the far nodeIt relays hop-by-hop to the edge gateway; each node relays once
Rebroadcast a duplicate idDedup drops it; no relay storm
Run a solar season cycle in heatBattery survives heat; node keeps sampling and relaying

Bench-test checklist. If a row fails, stop and fix it before moving on.

Expected output

The responder dashboard shows a map of nodes (green healthy, red alerting) and, on a detection, the originating node's location, fire score and the relay path out to the gateway.

jsonfire-alert.json
{
  "t": "fire",
  "node": 47,
  "lat": 30.41822,
  "lon": 78.09143,
  "score": 7.8,
  "id": 3080193,
  "hops": 3
}

Here node 47 deep in the forest has confirmed a fire (fused score 7.8) and its located alert has reached the edge gateway in three hops — a geolocated warning delivered in the fire's first minutes.

A LoRa radio transceiver module
A LoRa radio relays located alerts node-to-node out of the forest to a gateway and the fire service. Photograph sourced from Wikimedia Commons — LoRa module.jpg. Reused under the licence stated on that page; please check it before republishing.

Troubleshooting: Common Errors & Fixes

Frequent false alarms

Likely cause. Alerting on a single channel or absolute thresholds

Fix. Require multi-cue coincidence and persistence; use per-node baselines, not fixed limits

Node alarms on its own warm-up

Likely cause. Alerts enabled before baselines settled

Fix. Delay alerting for hours after deployment while baselines and sensor heaters stabilise

Alerts do not reach the gateway

Likely cause. Mesh gaps, HOP_MAX too low, or antenna orientation

Fix. Increase node density/HOP_MAX; raise and vertically orient antennas; add relay nodes

Nodes die in summer

Likely cause. Battery over-temperature

Fix. Use LiFePO4 with over-temp cutoff; shade the battery; oversize the panel for hot short days

Gas sensor drifts over months

Likely cause. Metal-oxide sensor ageing

Fix. Rely on rate-of-change (baseline) not absolute values; replace sensors periodically

The sketch will not upload — "Failed to connect" or "avrdude: stk500_recv()"

Likely cause. The bootloader is not being reached: wrong port, wrong board, a serial monitor holding the port open, or a USB cable that only carries power.

Fix. Close every serial monitor, confirm Tools → Board and Port, and swap to a known data-capable USB cable. On an ESP32 hold BOOT while the IDE prints "Connecting…", then release. If a peripheral is wired to the UART pins (GPIO 1/3 on ESP32, D0/D1 on Uno) unplug it — it fights the programmer.

The board resets in a loop, or the serial monitor prints "Brownout detector was triggered"

Likely cause. The supply cannot deliver peak current. Wi-Fi transmit bursts, relay coils and servos all pull far more than their average draw.

Fix. Power peripherals from a separate regulated supply with a common ground rather than from the board 5 V pin. Add a 470–1000 µF electrolytic capacitor across the supply near the load, and use a real power adapter rather than a laptop USB port.

Serial monitor shows garbage characters

Likely cause. Baud rate mismatch between Serial.begin() and the monitor, or a floating/shared UART line.

Fix. Set the monitor to 115200 to match the sketch. If it still garbles, the crystal or the USB bridge is being confused by noise — shorten the cable and keep motor wiring away from the USB lead.

An I²C device is not detected

Likely cause. Wrong address, missing pull-ups, swapped SDA/SCL, or a bus too long for the pull-up value.

Fix. Run an I²C scanner sketch first — it should print the device address. Most breakout boards include 4.7 kΩ pull-ups, but if you have chained four of them the parallel resistance is too low; remove the pull-ups from all but one board. Keep the bus under 30 cm at 100 kHz.

Wi-Fi connects but MQTT never does (state -2)

Likely cause. Wrong broker address or port, a firewall in the way, or the broker requiring credentials the sketch is not sending.

Fix. Test from a laptop on the same network first: mosquitto_sub -h <broker> -t "#" -v. If that works, the problem is on the device — check the IP literal, port 1883 (or 8883 for TLS), and that client.setServer() runs before connect(). PubSubClient state codes are documented in its header.

Readings arrive for a while and then stop

Likely cause. The Wi-Fi or MQTT session dropped and the sketch never reconnects, or the broker dropped the client on keep-alive timeout.

Fix. Never assume the link stays up. Check WiFi.status() and client.connected() at the top of every loop and reconnect with exponential backoff. Add a watchdog so a wedged network stack reboots the device instead of going silent.

Performance Optimisation

  • Deep-sleep between one-minute samples; the gas-sensor heaters are the main continuous draw, so manage warm-up carefully.
  • Keep nodes silent unless confirming — the mesh should carry almost no traffic until a real event.
  • Persist baselines in RTC memory so detection survives sleep without re-priming.
  • Bound mesh flooding with dedup, random backoff and a hop limit so alerts propagate fast without storms.
  • Replace every delay() with a millis() comparison — blocking delays are the single most common cause of dropped readings.
  • Sample sensors on a fixed cadence and publish on a slower one; you almost never need to transmit at the sampling rate.
  • Move networking into its own FreeRTOS task so a slow DNS lookup cannot stall the control loop.
  • Use uint8_t / uint16_t where the range allows; on an 8-bit AVR a 32-bit add costs four times as much.
  • Batch several samples into one MQTT publish. Radio time, not CPU time, dominates the energy budget.
  • Set the MQTT keep-alive to a value that matches your reporting interval so the broker does not churn reconnections.
  • For battery builds use deep sleep between samples: an ESP32 drops from ~160 mA awake to about 10 µA asleep, which is the difference between days and months of runtime.

Safety Precautions

  • This is a complement to satellites, cameras and lookouts — coverage depends on node density and it must not be the sole line of defence.
  • Design nodes and batteries for extreme heat with over-temperature protection; a node must not itself become an ignition or failure risk.
  • Conduct any smoke/flame testing with authorisation and full fire-safety precautions.
  • Ensure alerts reach a real dispatch path with human confirmation before mobilising resources.
  • Lithium cells vent and burn when abused. Only use protected cells or a proper BMS, never charge below 0 °C, and never leave a charging pack unattended on a wooden desk.
  • MQ-series sensors run a hot element. They get genuinely hot, need ventilation, and must never be enclosed in a sealed plastic box.
  • Never power an RF module without its antenna fitted — the reflected power destroys the output stage. Check your local licence-free band and duty-cycle limits before transmitting.
  • Wear eye protection when soldering or cutting, and solder in a ventilated space — rosin flux fumes are a respiratory irritant.
  • Power the circuit through a bench supply with a current limit while you are testing. A 300 mA limit turns a wiring mistake into a beep instead of a dead board.
  • Disconnect power before changing any wiring. Hot-plugging a sensor onto a live bus is the fastest way to lose a controller.

Maintenance

  • Replace ageing gas sensors and re-verify detection with a controlled test each season before the fire season.
  • Check battery health and over-temperature protection; heat degrades cells fastest.
  • Verify mesh connectivity and fill coverage gaps with additional relay nodes.
  • Keep solar panels clear of dust and canopy debris; a starved node is a blind spot.
  • Re-check every screw terminal and header after the first week — thermal cycling loosens connections that felt tight on day one.
  • Log pack voltage. When resting voltage after a full charge drops below about 4.0 V, the cell is near end of life — replace it.
  • Wash the panel every few weeks in dusty conditions; a visible dust film costs 15–25 % of the harvest.
  • Keep the broker and dashboard containers patched, and rotate device credentials at least once a year.
  • Recalibrate at the interval given in the calibration section, and keep the constants in a text file next to the firmware — not only in flash.
  • Keep a short logbook of firmware versions and what changed. Six months later you will not remember why that constant is 1.083.

Future Improvements & Upgrades

A working v1 is a platform, not a finish line. These are the upgrades that add the most capability for the least rework.

  • Add low-cost thermal or optical smoke imaging on select nodes for visual confirmation.
  • Correlate node detections with weather (wind, dryness indices) to predict spread direction with the alert.
  • Machine-learn the fusion weights per vegetation type from labelled fire/no-fire episodes.
  • Add satellite backhaul on gateways for forests beyond any cellular coverage.
  • Design a proper PCB. Once the breadboard version has run for a month, moving to a two-layer board removes the intermittent-contact failures that dominate prototype faults.
  • Add over-the-air firmware updates so you never have to physically reach a deployed node again.
  • Add persistent local storage (microSD or the on-chip flash) so a network outage does not create a hole in your data.
  • Move configuration out of the source: a captive-portal setup page or a JSON config file makes the build reusable without a recompile.
  • Add a battery and solar option so the unit survives a power cut and can be sited away from a socket.
  • Write a small test harness that feeds synthetic sensor values through the decision logic, so you can validate thresholds without physically triggering the event.

Frequently Asked Questions

How is this earlier than a satellite?

Satellites revisit on a schedule and need a fire large and hot enough to show through canopy and cloud. Ground nodes smell the smoke gas and feel the heat and humidity drop in the fire's first minutes, right at the source.

Won't cheap gas sensors cause endless false alarms?

Not when fused. The system requires smoke gas, temperature and humidity to move together and persist, judged against each node's own baseline. That coincidence is the fingerprint of fire and rejects weather, exhaust and drift.

How do alerts get out of a forest with no signal?

A LoRa mesh: each node relays its neighbours' messages, so an alert hops node to node until it reaches a gateway at the forest edge with cellular or wired backhaul to the fire service.

What is the flame sensor for if gas detects first?

Confirmation. Where a node has line of sight to flame, it adds strong direct evidence and raises confidence, but its short range and need for a clear view keep it a confirmer, not the primary detector.

How much area does one node cover?

A small radius — that is why density matters. Coverage is only as good as how many nodes you deploy, and the design is honest that it complements rather than replaces satellites and lookouts.

References & Learning Resources

These are the primary sources worth reading in full. Manufacturer datasheets always outrank forum posts when the two disagree.

  1. Wildfire detection methods — overviewReference
  2. Metal-oxide gas sensors (MQ series) — principlesReference
  3. LoRa mesh networking — overviewReference
  4. Sensor fusion for detection — overviewReference
  5. FAO — forest fire managementFAO