Community project

Forest Fire Detector

ESP32
Photo of Forest Fire Detector
Generated with AI

Toumi Belkacem

Published October 6, 2026

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

Wiring diagram for Forest Fire Detector

Gather all the parts

QtyComponent
1

KY-026 Flame Sensor Module

A flame-light module whose AO output is measured by the separate ADS1115 converter.

1

Buzzer

Piezo buzzer for sound output

1

LED

rouge

Standard 3mm/5mm through-hole LED. A current-limiting series resistor is added automatically.

1

Resistor

220 Ω

Through-hole resistor (current-limiting in series with an LED)

1

Gravity IP65 Capacitive Soil Moisture Sensor

A weather-resistant soil moisture probe whose output is measured by the separate ADS1115 converter.

1

SX1278 LoRa RF Module (Ra-02)

Ai-Thinker Ra-02 style LoRa module based on Semtech SX1278, typically used at 433 MHz. Uses SPI plus reset and DIO interrupt pins. Operates at 3.3V logic only.

1

6V 4W Solar Panel

Polycrystalline or monocrystalline solar panel rated 4W peak output at 6V nominal. Typical small-panel values are Vmp about 6V and Imp about 667mA; open-circuit voltage is higher than the nominal working voltage and must stay within the charger module input rating. Two-wire output (V+ and GND). Outdoor/weatherproof rating depends on the exact panel. Directly compatible with CN3791 MPPT solar charger modules only when that module's input-voltage rating is not exceeded. No firmware library required — purely a passive power source.

1

18650 Li-ion Cell

18650 lithium-ion cell, nominal 3.7 V, ~2500 mAh. Common for higher-capacity portable / battery-bank style projects; needs a holder and protection / charger circuit.

1

Adafruit bq25185 USB / DC / Solar Charger with 3.3V Buck Board

bq25185 multi-source LiPoly/LiIon charger combined with a 3.3V buck regulator output, letting a project run directly from the board without a separate regulator.

1

MicroSD Card Module

SPI-based microSD card adapter module for SPI-capable microcontrollers. Uses MOSI, MISO, SCK, and CS plus power and ground. Many low-cost modules include a 3.3 V regulator and level shifting for 5 V MCU boards, while bare breakouts should be powered and signalled at 3.3 V.

1

BME280

Bosch BME280 environmental sensor on the exact Adafruit 2652 breakout. Supports I2C via SCK/SCL and SDI/SDA, optional SDO address select, and SPI pins when needed.

1

ADS1115 16-Bit ADC Module

16-bit four-channel I2C analog-to-digital converter with programmable gain amplifier. Common ADS1115 breakouts expose MCU-facing SDA/SCL pins and four analog inputs for single-ended or differential measurements.

1

GY-82 10-DOF Sensor Module

A combined motion, magnetic-field, and air-pressure module used to notice impacts, tilt, and local movement.

1

Cup anemometer with reed-switch output

A three-cup outdoor wind-speed sensor that closes a small switch once per rotation.

1

Tipping-bucket rain gauge with reed-switch output

A self-emptying rain collector that closes a small switch for every measured tip of rain.

1

UV-stable Fresnel half-dome optical collector

A weather-protected half-dome Fresnel optical part that concentrates light from the monitored horizon toward the flame sensor and camera window.

1

MCP23017 I/O Expander

Microchip MCP23017 16-bit GPIO expander controlled over I2C. Provides two 8-bit ports for external digital inputs or outputs.

1

Resistor 100 kΩ (battery divider, upper)

100 kΩ

The upper passive resistor that safely reduces battery voltage before measurement.

1

Resistor 100 kΩ (battery divider, lower)

100 kΩ

The lower passive resistor that completes the protected battery measurement divider to ground.

1

DS3231 RTC module, 3.3 V I²C configuration

A battery-backed clock that keeps accurate timestamps while the field node sleeps or loses main power.

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"

Functionsolar_panelESP32
groundGND → Adafruit bq25185 USB / DC / Solar Charger with 3.3V Buck Board GNDEXT
powerV+ → Adafruit bq25185 USB / DC / Solar Charger with 3.3V Buck Board VINEXT

2. Connections between "battery_18650" and "ESP32"

Functionbattery_18650ESP32
power+V → Adafruit bq25185 USB / DC / Solar Charger with 3.3V Buck Board BATEXT
groundGND → Adafruit bq25185 USB / DC / Solar Charger with 3.3V Buck Board GNDEXT

3. Connections between "solar_power_manager" and "ESP32"

Functionsolar_power_managerESP32
power3V33V3
groundGNDGND

4. Connections between "bme280_env" and "ESP32"

Functionbme280_envESP32
powerVCC3V3
groundGNDGND
i2cSDAGPIO 8
i2cSCLGPIO 9

5. Connections between "gy82_motion" and "ESP32"

Functiongy82_motionESP32
powerVCC3V3
groundGNDGND
i2cSDAGPIO 8
i2cSCLGPIO 9

6. Connections between "ads1115_adc" and "ESP32"

Functionads1115_adcESP32
powerVDD3V3
groundGNDGND
i2cSDAGPIO 8
i2cSCLGPIO 9

7. Connections between "flame_sensor" and "ESP32"

Functionflame_sensorESP32
powerVCC3V3
groundGNDGND
analogAO → ADS1115 16-Bit ADC Module AIN0EXT

8. Connections between "soil_sensor" and "ESP32"

Functionsoil_sensorESP32
powerVCC3V3
groundGNDGND
analogAOUT → ADS1115 16-Bit ADC Module AIN1EXT

9. Connections between "lora_radio" and "ESP32"

Functionlora_radioESP32
powerVCC3V3
groundGNDGND
spiMOSIGPIO 11
spiMISOGPIO 13
spiSCKGPIO 12
spiNSSGPIO 10
digitalDIO0GPIO 40
powerRESET3V3

10. Connections between "microsd_log" and "ESP32"

Functionmicrosd_logESP32
powerVCC3V3
groundGNDGND
spiMOSIGPIO 11
spiMISOGPIO 13
spiSCKGPIO 12
spiCSGPIO 41

11. Connections between "alarm_buzzer" and "ESP32"

Functionalarm_buzzerESP32
groundGNDGND
digitalSIGNALGPIO 38

12. Connections between "led_resistor" and "ESP32"

Functionled_resistorESP32
digitalP2GPIO 39
digitalP1 → LED ANODEEXT

13. Connections between "warning_led" and "ESP32"

Functionwarning_ledESP32
groundGNDGND

14. Connections between "wind_anemometer" and "ESP32"

Functionwind_anemometerESP32
groundSW2GND
digitalSW1 → MCP23017 I/O Expander GPA0-GPA7EXT

15. Connections between "rain_gauge" and "ESP32"

Functionrain_gaugeESP32
groundSW2GND
digitalSW1 → MCP23017 I/O Expander GPA0-GPA7EXT

16. Connections between "fresnel_optical_dome" and "ESP32"

Functionfresnel_optical_domeESP32
dataOPTICAL_AXIS → KY-026 Flame Sensor Module AOEXT

17. Connections between "weather_gpio_expander" and "ESP32"

Functionweather_gpio_expanderESP32
powerVDD3V3
groundVSSGND
i2cSDAGPIO 8
i2cSCLGPIO 9
powerRESET3V3

18. Connections between "rtc_clock" and "ESP32"

Functionrtc_clockESP32
powerVCC3V3
groundGNDGND
i2cSDAGPIO 8
i2cSCLGPIO 9

19. Connections between "battery_divider_top" and "ESP32"

Functionbattery_divider_topESP32
digitalP1 → 18650 Li-ion Cell +VEXT
digitalP2 → ADS1115 16-Bit ADC Module AIN2EXT

20. Connections between "battery_divider_bottom" and "ESP32"

Functionbattery_divider_bottomESP32
digitalP1 → ADS1115 16-Bit ADC Module AIN2EXT
groundP2GND

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.

Open in Schematik