Overview
In this beginner project you will connect an AS3935 lightning sensor module to the ESP32 via SPI and configure it to detect cloud-to-ground and intra-cloud lightning. When a strike is detected, the estimated distance (1-40 km) is printed to Serial and an LED flashes rapidly. The sensor filters disturbers (electrical noise from motors and fluorescent lights) automatically.
Components
- 1× ESP32 DevKit V1
- 1× AS3935 lightning sensor module — DFRobot Gravity or SparkFun; SPI or I2C
- 1× Red LED and 220 ohm resistor — Strike indicator
- 1× SSD1306 OLED 128x64 I2C
Wiring
| Component Pin | ESP32 Pin | Notes |
|---|---|---|
| AS3935 CS/SCK/MOSI/MISO | GPIO 5/18/23/19 | SPI interface |
| AS3935 IRQ | GPIO 4 | Interrupt; strike event signal |
| AS3935 VCC/GND | 3.3 V / GND | 3.3 V only; not 5 V tolerant |
| OLED SDA/SCL | GPIO 21/22 | |
| Red LED anode via 220R | GPIO 26 |
Arduino Code
// ESP32 Lightning Detector - Beginner
// AS3935 SPI -> interrupt-driven strike detection with distance estimate
// Library: SparkFun AS3935 Lightning Detector Arduino Library
#include <Wire.h>
#include <Adafruit_SSD1306.h>
#include <SPI.h>
#include <SparkFun_AS3935.h>
#define AS3935_CS 5
#define AS3935_IRQ 4
#define LED_PIN 26
SparkFun_AS3935 lightning(AS3935_CS);
Adafruit_SSD1306 oled(128, 64, &Wire, -1);
volatile bool strikeDetected = false;
void IRAM_ATTR onStrike() { strikeDetected = true; }
void setup() {
Serial.begin(115200);
Wire.begin(21, 22);
oled.begin(SSD1306_SWITCHCAPVCC, 0x3C); oled.setTextColor(WHITE);
SPI.begin();
pinMode(AS3935_IRQ, INPUT);
pinMode(LED_PIN, OUTPUT);
attachInterrupt(digitalPinToInterrupt(AS3935_IRQ), onStrike, RISING);
if (!lightning.beginSPI(AS3935_CS, 2000000)) {
Serial.println("AS3935 not detected"); while (1);
}
// Indoor mode reduces antenna gain for less false positives in buildings
lightning.setIndoorOutdoor(INDOOR);
// Disturber rejection: masks false triggers from noise sources
lightning.maskDisturber(true);
// Spike rejection (0-15): higher = stricter; default 2
lightning.spikeRejection(2);
// Noise floor level (0-7): increase if frequent false positives
lightning.setNoiseLevel(2);
Serial.println("Lightning detector armed. Watching for storms...");
oled.clearDisplay(); oled.setCursor(0,0); oled.setTextSize(1);
oled.println("Lightning Detector"); oled.println("Watching..."); oled.display();
}
void loop() {
if (!strikeDetected) return;
strikeDetected = false;
delay(5); // brief delay for AS3935 to update registers
int intType = lightning.readInterruptReg();
if (intType == NOISE_INT) {
Serial.println("Noise detected — increase noise floor level");
} else if (intType == DISTURBER_INT) {
Serial.println("Disturber detected (electrical noise, not lightning)");
} else if (intType == LIGHTNING_INT) {
int dist = lightning.distanceToStorm();
int energy = lightning.lightningEnergy();
Serial.printf("LIGHTNING! Distance: %d km Energy: %dn", dist, energy);
// Flash LED
for (int i = 0; i < 5; i++) {
digitalWrite(LED_PIN, HIGH); delay(50);
digitalWrite(LED_PIN, LOW); delay(50);
}
oled.clearDisplay(); oled.setCursor(0,0); oled.setTextSize(1);
oled.println("LIGHTNING DETECTED!");
oled.printf("Distance: ~%d kmn", dist);
oled.printf("Energy: %dn", energy);
oled.display();
}
}How It Works
AS3935 RF Detection Principle: The AS3935 contains a tuned antenna resonating at 500 kHz that detects the broadband radio frequency burst (sferic) emitted by lightning discharge. The IC applies a matched filter and statistical algorithm to distinguish genuine lightning sferics from narrowband interference sources like motors, switching supplies, and fluorescent lights.
Interrupt-Driven Detection: The AS3935 IRQ pin goes HIGH for approximately 1 ms when any event is detected (noise, disturber, or lightning). The interrupt service routine sets a strikeDetected flag. The main loop checks this flag and reads the interrupt register to determine which event type occurred. The 5 ms delay allows the AS3935 registers to stabilise after IRQ.
Distance Estimation Algorithm: The AS3935 estimates distance to the storm front based on the signal strength and time-frequency signature of the sferic. It returns 14 distance values from 1 km (overhead) to 40 km (storm approaching from distant horizon) plus a value indicating the storm is overhead. Accuracy is approximately +/-3 km at 20 km.
Indoor vs Outdoor Mode: Indoor mode reduces the antenna gain by 6 dB to decrease sensitivity, reducing false positives from building electrical noise. Outdoor mode uses full gain for maximum range. For an outdoor-mounted detector connected to an ESP32 indoors via long wires, use outdoor mode with increased noise floor level to compensate for wire-conducted interference.
Applications
- Outdoor worker safety alert system for approaching electrical storms
- Golf course automatic lightning siren controller
- Drone flight safety automatic ground recall trigger
- Amateur radio station antenna disconnection automation on lightning alert
Troubleshooting
Frequent DISTURBER_INT interrupts with no storms
Disturbers are electrical noise sources: fluorescent lights, switching power supplies, AC motors. Move the AS3935 away from these sources. Increase spikeRejection() from 2 to 4 or 5. If disturbers persist, keep maskDisturber(true) to prevent the disturber interrupt and only respond to confirmed lightning events.
No interrupts at all even during confirmed thunderstorms
Verify the IRQ pin wiring and the interrupt attachment. Print the AS3935 register dump using lightning.printAllRegs() to verify communication. Ensure the antenna calibration is correct: call lightning.tuneCap() and check the returned value is between 0-15.
Distance always reads 63 km (out of range)
The distance register returns 63 when the storm is estimated to be over 40 km away or the energy was too low for a reliable distance estimate. This is normal for weak distant strikes. Readings will become more specific as the storm approaches within 40 km.
Upgrades
- Add a buzzer with increasing alarm rate as storm distance decreases
- Add a second AS3935 outdoors and compare indoor/outdoor readings to improve accuracy
- Add Wi-Fi and Telegram alerts with the estimated distance and time
- Add NVS logging of all strike events with timestamps for storm tracking
FAQ
You need an ESP32 DevKit, AS3935 CS/SCK/MOSI/MISO, AS3935 IRQ, a breadboard, jumper wires, and a USB cable for power and programming.
Only the Advanced stage uses Wi-Fi. Beginner and Intermediate builds run offline on the ESP32 with USB power.
Start with Beginner if you are new to Environmental. Use Intermediate for OLED feedback and Advanced for dashboards or connected monitoring.
Overview
The intermediate build logs all strike events with NTP timestamps and estimated distances to NVS. A Telegram alert fires within seconds of each detected strike, including the distance and whether the storm appears to be approaching (decreasing distances) or retreating. An OLED shows a text storm-distance timeline for the last five strikes.
Components
- 1× ESP32 DevKit V1
- 1× AS3935 module
- 1× SSD1306 OLED
- 1× Wi-Fi router — NTP + Telegram
Wiring
| Component Pin | ESP32 Pin | Notes |
|---|---|---|
| Same as beginner | AS3935 SPI + IRQ + OLED I2C |
Arduino Code
// ESP32 Lightning Detector - Intermediate (NTP log + Telegram + storm trend)
#include <Wire.h>
#include <Adafruit_SSD1306.h>
#include <SPI.h>
#include <SparkFun_AS3935.h>
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <time.h>
#define AS3935_CS 5
#define AS3935_IRQ 4
SparkFun_AS3935 lightning(AS3935_CS);
Adafruit_SSD1306 oled(128,64,&Wire,-1);
const char* SSID="YourSSID", *PASS="YourPass";
const char* BOT_TOKEN="YOUR_BOT_TOKEN";
const char* CHAT_ID="YOUR_CHAT_ID";
volatile bool strike=false;
void IRAM_ATTR onStrike(){ strike=true; }
struct StrikeLog { time_t ts; int dist; };
StrikeLog history[5]; int histIdx=0;
void sendTelegram(const String &msg){
WiFiClientSecure c; c.setInsecure();
if(!c.connect("api.telegram.org",443)) return;
String body="chat_id="+String(CHAT_ID)+"&text="+msg;
c.printf("POST /bot%s/sendMessage HTTP/1.1rnHost: api.telegram.orgrn"
"Content-Type: application/x-www-form-urlencodedrnContent-Length: %drnrn%s",
BOT_TOKEN,(int)body.length(),body.c_str());
delay(1000); c.stop();
}
void updateOLED(){
oled.clearDisplay(); oled.setCursor(0,0); oled.setTextSize(1);
oled.println("Recent strikes (km):");
for(int i=0;i<5;i++){
int idx=(histIdx-1-i+5)%5;
if(history[idx].ts) oled.printf("%d km agon",history[idx].dist);
}
oled.display();
}
void setup(){
Serial.begin(115200);
Wire.begin(21,22);
oled.begin(SSD1306_SWITCHCAPVCC,0x3C); oled.setTextColor(WHITE);
SPI.begin();
pinMode(AS3935_IRQ,INPUT);
attachInterrupt(digitalPinToInterrupt(AS3935_IRQ),onStrike,RISING);
lightning.beginSPI(AS3935_CS,2000000);
lightning.setIndoorOutdoor(INDOOR);
lightning.maskDisturber(true);
WiFi.begin(SSID,PASS); while(WiFi.status()!=WL_CONNECTED) delay(500);
configTime(0,0,"pool.ntp.org");
Serial.printf("Lightning detector: %sn",WiFi.localIP().toString().c_str());
}
void loop(){
if(!strike) return;
strike=false; delay(5);
if(lightning.readInterruptReg()!=LIGHTNING_INT) return;
int dist=lightning.distanceToStorm();
time_t now=time(nullptr);
history[histIdx%5]={now,dist}; histIdx++;
// Check trend: is storm approaching?
bool approaching=false;
if(histIdx>=2){
int prev=history[(histIdx-2+5)%5].dist;
int curr=history[(histIdx-1+5)%5].dist;
approaching=(curr<prev);
}
String msg="LIGHTNING! ~"+String(dist)+"km "+(approaching?"APPROACHING":"retreating");
struct tm ti; localtime_r(&now,&ti);
char ts[20]; strftime(ts,sizeof(ts),"%H:%M:%S",&ti);
msg+=" ("+String(ts)+")";
Serial.println(msg);
sendTelegram(msg);
updateOLED();
}How It Works
Storm Trend Detection: The history[] circular buffer stores the last five strike distances. After each strike, the current distance is compared to the previous strike distance. If current is smaller than previous, the storm is approaching. This simple delta-based trend gives actionable direction information that a single distance reading cannot provide.
NTP Timestamps for Alerts: configTime() syncs the ESP32 clock from the NTP pool. localtime_r() converts the Unix timestamp to local time for the alert message. Including the detection time in the Telegram message allows correlation with official weather radar and provides audit records if the strike caused damage.
Circular Strike History Buffer: The five-element history[] array with histIdx incremented on each strike stores the most recent five events. The display iterates back from histIdx-1 to show events in reverse chronological order. This provides a visual trend without persistent storage — the history clears on power cycle.
Disturber Masking in Wi-Fi Environments: Wi-Fi radio frequency bursts can occasionally trigger AS3935 disturber interrupts. maskDisturber(true) prevents disturber events from reaching the main code. The ESP32 Wi-Fi transceiver operates at 2.4 GHz, far from the 500 kHz lightning detection band, but switch-mode converters on the development board may generate broadband interference.
Applications
- Campsite or outdoor event lightning safety alert system
- Automated irrigation system shutdown on storm approach
- Lightning rod effectiveness monitoring station
- Citizen science lightning detection network for storm tracking
Troubleshooting
Storm trend always shows approaching even for retreating storm
The AS3935 distance estimate has variance between strikes; two consecutive measurements from the same storm may fluctuate. Average the last three distances before comparing trend. Require at least three consecutive decreasing readings before classifying as approaching.
Telegram alert arrives 10-30 seconds after interrupt
The WiFiClientSecure TLS handshake takes 1-3 seconds. The 1000 ms delay after sending adds more latency. Reduce latency by keeping the Telegram connection open with Connection: keep-alive or switch to a lightweight HTTP client without TLS (use http instead of https for non-sensitive alerts).
Upgrades
- Add an NVS log that persists across power cycles for post-storm analysis
- Add a Home Assistant webhook so the lightning event can trigger automations like closing skylights
- Add GPS to share your location with the Telegram alert for remote-site safety monitoring
- Add multiple detectors at different locations and triangulate the strike position
FAQ
You need an ESP32 DevKit, AS3935 CS/SCK/MOSI/MISO, AS3935 IRQ, a breadboard, jumper wires, and a USB cable for power and programming.
Only the Advanced stage uses Wi-Fi. Beginner and Intermediate builds run offline on the ESP32 with USB power.
Start with Beginner if you are new to Environmental. Use Intermediate for OLED feedback and Advanced for dashboards or connected monitoring.
Overview
The advanced build publishes strike events to MQTT in real time, with Node-RED plotting each strike on a time-distance chart. Strike energy values are accumulated to estimate storm intensity. When a storm is detected approaching within 15 km, the ESP32 publishes a high-priority MQTT alert that triggers Home Assistant automations: closing Velux windows, activating an outdoor siren, and pausing lawn irrigation.
Components
- 1× ESP32 DevKit V1
- 1× AS3935 module
- 1× MQTT broker + Node-RED
- 1× Home Assistant — Automation on proximity alert
Wiring
| Component Pin | ESP32 Pin | Notes |
|---|---|---|
| Same as intermediate |
Arduino Code
// ESP32 Lightning Detector - Advanced (MQTT + storm proximity automation)
#include <SPI.h>
#include <SparkFun_AS3935.h>
#include <WiFi.h>
#include <PubSubClient.h>
#include <ArduinoJson.h>
#include <time.h>
#define AS3935_CS 5
#define AS3935_IRQ 4
SparkFun_AS3935 lightning(AS3935_CS);
WiFiClient wc; PubSubClient mqtt(wc);
const char* SSID="YourSSID", *PASS="YourPass";
const char* MQTT_HOST="192.168.1.100";
const int PROXIMITY_KM=15; // trigger automation within 15 km
volatile bool strike=false;
void IRAM_ATTR onStrike(){ strike=true; }
int recentDist[10]; int distIdx=0;
bool proximityAlertActive=false;
void publishStrike(int dist, int energy){
StaticJsonDocument<128> doc;
doc["dist"]=dist; doc["energy"]=energy; doc["ts"]=(long)time(nullptr);
char buf[128]; serializeJson(doc,buf);
mqtt.publish("lightning/strike",buf);
Serial.printf("Strike: %dkm energy:%dn",dist,energy);
}
void checkProximity(int dist){
recentDist[distIdx%10]=dist; distIdx++;
if(dist<=PROXIMITY_KM&&!proximityAlertActive){
mqtt.publish("lightning/proximity","DANGER");
proximityAlertActive=true;
Serial.println("PROXIMITY ALERT published");
}
if(dist>PROXIMITY_KM+10&&proximityAlertActive){
mqtt.publish("lightning/proximity","CLEAR");
proximityAlertActive=false;
}
}
void setup(){
Serial.begin(115200);
SPI.begin();
pinMode(AS3935_IRQ,INPUT);
attachInterrupt(digitalPinToInterrupt(AS3935_IRQ),onStrike,RISING);
lightning.beginSPI(AS3935_CS,2000000);
lightning.setIndoorOutdoor(INDOOR);
lightning.maskDisturber(true);
WiFi.begin(SSID,PASS); while(WiFi.status()!=WL_CONNECTED) delay(500);
configTime(0,0,"pool.ntp.org");
mqtt.setServer(MQTT_HOST,1883);
}
void loop(){
if(!mqtt.connected()) mqtt.connect("LightningDet");
mqtt.loop();
if(!strike) return;
strike=false; delay(5);
if(lightning.readInterruptReg()!=LIGHTNING_INT) return;
int dist=lightning.distanceToStorm();
int energy=lightning.lightningEnergy();
publishStrike(dist,energy);
checkProximity(dist);
}How It Works
Proximity Alert Hysteresis: The proximity alert activates when any strike registers within PROXIMITY_KM (15 km). It clears only when a strike registers beyond PROXIMITY_KM+10 (25 km), providing a 10 km hysteresis band. This prevents the alert from flickering on and off as the storm distance estimate fluctuates around the threshold.
MQTT Topic Separation: lightning/strike receives every detected strike as a JSON object for logging and charting. lightning/proximity receives only the high-priority state change (DANGER or CLEAR). Separating topics allows consumers to subscribe selectively: Node-RED subscribes to strike for charting; Home Assistant subscribes to proximity for automation.
Home Assistant Automation Trigger: An HA MQTT trigger on lightning/proximity matching "DANGER" fires an automation sequence: service call to close cover entities (skylights), switch on siren switch, and turn off irrigation switch. The CLEAR payload fires the reverse automation. All in under 1 second of the MQTT message arrival.
Strike Energy Accumulation: The AS3935 lightningEnergy() returns a 21-bit value proportional to the electromagnetic energy of the detected sferic. Summing energy values over a window indicates storm intensity. Node-RED processes the accumulated energy series and renders it as a storm intensity bar chart alongside the distance time series.
Applications
- Smart home storm protection automating windows, gates, and outdoor equipment
- Golf course or sports venue automated safety siren and PA announcement trigger
- Outdoor wedding or event venue lightning safety management system
- IoT lightning strike density mapping network for meteorological research
Troubleshooting
Proximity alert fires and never clears
The clear condition requires a strike to be detected beyond 25 km. If the storm passes directly overhead and no subsequent strikes are detected, the alert stays active indefinitely. Add a 30-minute timer that clears the alert if no strikes are detected within the timeout.
Home Assistant does not respond to lightning/proximity messages
Verify the MQTT integration is connected in HA Settings. Create a Manual Trigger in the automation with Platform: MQTT, Topic: lightning/proximity, Payload: DANGER. Test the trigger by manually publishing "DANGER" to the topic from MQTT Explorer before relying on the live detector.
Upgrades
- Add multiple detector nodes at different sites and publish to a common MQTT topic for distributed coverage
- Add a lightning strike heatmap in Node-RED using a world-map node with strike GPS coordinates from detector location metadata
- Add integration with the Blitzortung.org open lightning detection network for comparison
- Add a battery backup so the detector continues operating during storm-induced power outages
FAQ
You need an ESP32 DevKit, AS3935 CS/SCK/MOSI/MISO, AS3935 IRQ, a breadboard, jumper wires, and a USB cable for power and programming.
Only the Advanced stage uses Wi-Fi. Beginner and Intermediate builds run offline on the ESP32 with USB power.
Start with Beginner if you are new to Environmental. Use Intermediate for OLED feedback and Advanced for dashboards or connected monitoring.