Flood Monitoring & Auto Power Cut-Off System
Arduino source code (NodeMCU ESP8266) with full explanatory comments — file: flood_monitor_explained.ino
/*
* ============================================================================
* FLOOD MONITORING & AUTOMATIC POWER CUT-OFF SYSTEM
* Fully commented (educational) version
* ============================================================================
*
* WHAT THIS SYSTEM DOES (in plain English):
* -----------------------------------------
* 1. A rain sensor on the roof detects the moment rain starts falling.
* 2. A soil moisture sensor (optional add-on) in the ground detects when
* the soil is so soaked ("saturated") that it can no longer absorb
* water — the condition that turns ordinary rain into a flood. The
* system runs perfectly without this sensor connected.
* 3. An HC-SR04 ultrasonic sensor mounted high up (e.g. on the ceiling
* of a veranda) points DOWN at the floor and measures the distance to
* whatever is below it. When water enters, the distance gets SHORTER.
* Water level = mounting height - measured distance.
* 4. The system escalates through 3 alert stages, each with a voice
* message played 3 times through a speaker:
* Stage 1: "Light rain detected" (rain or saturated soil)
* Stage 2: "Flood warning" (water 10 cm above floor)
* Stage 3: "Evacuate now" (water 25 cm above floor)
* 5. At Stage 3, BEFORE the water reaches the wall sockets, relay 1 cuts
* the AC supply line and relay 2 switches on an external siren.
* 6. Sensor readings are uploaded to the internet (ThingSpeak) over
* Wi-Fi so the flood can be monitored remotely on a phone.
*
* HOW TO READ THIS FILE:
* ----------------------
* The code runs from top to bottom in this order:
* setup() -> runs ONCE when the board powers on (prepares pins, etc.)
* loop() -> runs FOREVER, over and over, thousands of times a second
* Everything else is a "function" — a named block of code that setup()
* and loop() call when they need it.
* ============================================================================
*/
/* ---------------------------------------------------------------------------
* LIBRARIES
* A library is pre-written code that someone else made, which we "include"
* so we don't have to write it ourselves.
* ------------------------------------------------------------------------ */
#include <ESP8266WiFi.h> // Lets the NodeMCU connect to Wi-Fi networks
#include <SoftwareSerial.h> // Creates an extra serial port on normal pins
// (needed because the DFPlayer talks over
// serial, and the main serial port is busy
// talking to the computer for debugging)
#include <DFRobotDFPlayerMini.h> // Controls the DFPlayer Mini MP3 module
// Install via: Sketch > Include Library >
// Manage Libraries > search "DFRobotDFPlayerMini"
/* ---------------------------------------------------------------------------
* USER SETTINGS — these are the ONLY values you should need to change
* ------------------------------------------------------------------------ */
// Set to true if you want the system to upload readings to ThingSpeak.
// Set to false to run completely offline (the alerts still work!).
#define ENABLE_CLOUD false
const char* WIFI_SSID = "YourWiFi"; // Your Wi-Fi network name
const char* WIFI_PASS = "YourPassword"; // Your Wi-Fi password
const char* TS_API_KEY = "YOUR_KEY_HERE"; // From [Link] (free account)
// How high above the FLOOR the ultrasonic sensor is mounted, in centimetres.
Flood Monitoring System - flood_monitor_explained.ino Page 1
// MEASURE THIS WITH A TAPE RULE after you mount the sensor. If the sensor
// hangs 250 cm above the floor and it measures 240 cm to the surface below,
// then the water is 250 - 240 = 10 cm deep.
const float MOUNT_HEIGHT_CM = 250.0;
// The water depths (measured from the floor) that trigger each stage:
const float LEVEL_WARNING_CM = 10.0; // Stage 2 fires at 10 cm of water
const float LEVEL_CRITICAL_CM = 25.0; // Stage 3 fires at 25 cm of water
// (set this BELOW your lowest socket!)
// "Hysteresis" stops the alarm from rapidly switching on/off when the water
// hovers exactly at a threshold (e.g. small waves). Once the WARNING alarm
// is on, the water must drop 3 cm BELOW the threshold before it turns off.
const float HYSTERESIS_CM = 3.0;
// Soil moisture threshold. Capacitive sensors give a LOWER number when WET.
// CALIBRATE IT: open the Serial Monitor, note the value with the probe in
// dry soil (about 700-800) and in a cup of water (about 300-400), then pick
// a number in between.
const int SOIL_WET_RAW = 450;
/* ---------------------------------------------------------------------------
* PIN MAP — which wire goes to which pin on the NodeMCU
* "#define NAME value" just gives a pin a human-readable name.
* ------------------------------------------------------------------------ */
#define PIN_TRIG D1 // Ultrasonic TRIG: we pulse this to fire a sound ping
#define PIN_ECHO D2 // Ultrasonic ECHO: goes HIGH until the echo returns.
// !!! MUST pass through a voltage divider (1k + 2k)
// because the sensor outputs 5V and the ESP8266
// pins can only survive 3.3V !!!
#define PIN_RAIN D3 // FC-37 rain sensor DIGITAL output (LOW when wet)
#define PIN_DF_RX D5 // We LISTEN here -> connect to DFPlayer TX pin
#define PIN_DF_TX D6 // We TALK here -> connect to DFPlayer RX pin
// (put a 1k resistor in series on this wire — the
// DFPlayer expects 5V logic, 3.3V works but the
// resistor stops noise/clicking sounds)
#define PIN_RELAY_AC D7 // Relay channel 1: cuts the AC supply line
#define PIN_SIREN D0 // Relay channel 2: switches the external siren
#define PIN_BUZZER D8 // Small active buzzer (backup if speaker fails)
#define PIN_LED D4 // Red warning LED (shares the onboard LED, which
// is wired "inverted": LOW = light ON)
#define PIN_SOIL A0 // The ONLY analog pin on the ESP8266 — used by
// the capacitive soil moisture sensor
// Most cheap 4-channel relay boards are "ACTIVE LOW": sending LOW switches
// the relay ON. If your board is the opposite, just swap these two lines.
#define RELAY_ON LOW
#define RELAY_OFF HIGH
/* ---------------------------------------------------------------------------
* THE STATE MACHINE
* Instead of dozens of tangled if-statements, the system is always in
* exactly ONE of four "states". Each loop, we look at the sensors and
* decide whether to move to a different state. This makes the logic easy
* to reason about and easy to extend.
* ------------------------------------------------------------------------ */
enum SystemState {
STATE_NORMAL, // dry, calm, nothing happening
STATE_RAIN, // rain detected OR soil saturated — early warning
STATE_WARNING, // water is physically rising on the floor
STATE_CRITICAL // flood level — power cut, evacuate
};
SystemState state = STATE_NORMAL; // we always start assuming it's dry
// SAFETY LATCH: once we reach CRITICAL and cut the power, this flag keeps
// us there even if the water drops again. Wet sockets must NEVER get power
// back automatically — a human must inspect and press the reset button.
bool criticalLatched = false;
// Track numbers of the MP3 files on the DFPlayer's SD card.
Flood Monitoring System - flood_monitor_explained.ino Page 2
// The SD card must contain a folder named "mp3" with files named EXACTLY:
// 0001.mp3, 0002.mp3, 0003.mp3
#define TRACK_LIGHT_RAIN 1 // "Light rain detected, stay alert"
#define TRACK_FLOOD_WARN 2 // "Flood warning, water level rising"
#define TRACK_EVACUATE 3 // "Evacuate now, evacuate now"
#define VOICE_REPEATS 3 // each message is played 3 times
/* ---------------------------------------------------------------------------
* GLOBAL OBJECTS AND VARIABLES
* "Global" = visible to every function in the file.
* ------------------------------------------------------------------------ */
SoftwareSerial dfSerial(PIN_DF_RX, PIN_DF_TX); // the wire to the DFPlayer
DFRobotDFPlayerMini dfPlayer; // the DFPlayer driver object
bool dfOk = false; // did the DFPlayer start OK?
// millis() returns how many milliseconds the board has been on. By storing
// the time of our last action and comparing, we can do things "every N
// seconds" WITHOUT using delay() — delay() freezes the whole program, which
// would make the buzzer and LED stutter.
unsigned long lastRead = 0; // when we last read the sensors
unsigned long lastUpload = 0; // when we last uploaded to the cloud
unsigned long lastBeep = 0; // when we last toggled the buzzer/LED
const unsigned long READ_INTERVAL = 2000; // read sensors every 2 s
const unsigned long UPLOAD_INTERVAL = 30000; // upload every 30 s
bool beepOn = false; // is the buzzer/LED currently on or off?
// The latest sensor readings, shared between functions:
float waterLevelCm = 0; // water depth above the floor, in cm
bool rainDetected = false; // true while the FC-37 senses rain
int soilRaw = 1024; // raw soil moisture (1024 = totally dry)
/* ===========================================================================
* setup() — runs ONCE at power-on
* ======================================================================== */
void setup() {
// Open the USB serial port so we can print debug messages to the
// computer (Tools > Serial Monitor, set to 115200 baud).
[Link](115200);
// Tell the chip which pins are inputs and which are outputs.
pinMode(PIN_TRIG, OUTPUT);
pinMode(PIN_ECHO, INPUT);
pinMode(PIN_RAIN, INPUT_PULLUP); // PULLUP = pin reads HIGH by default;
// the sensor pulls it LOW when wet
pinMode(PIN_RELAY_AC, OUTPUT);
pinMode(PIN_SIREN, OUTPUT);
pinMode(PIN_BUZZER, OUTPUT);
pinMode(PIN_LED, OUTPUT);
/* FAIL-SAFE STARTING POSITION:
* Relay 1 starts OFF. The AC line is wired through the relay's
* NC (Normally Closed) contact, so with the relay OFF, power flows
* normally. We only ENERGIZE the relay to break the line and cut power.
* Why this direction? If the ESP8266 ever crashes or loses power, the
* building keeps its electricity — the system "fails safe". */
digitalWrite(PIN_RELAY_AC, RELAY_OFF);
digitalWrite(PIN_SIREN, RELAY_OFF);
digitalWrite(PIN_BUZZER, LOW);
digitalWrite(PIN_LED, HIGH); // remember: onboard LED is inverted, HIGH = off
// Start talking to the DFPlayer at 9600 baud (its fixed speed).
[Link](9600);
if ([Link](dfSerial)) {
dfOk = true;
[Link](28); // volume range is 0 (silent) to 30 (maximum)
[Link](F("DFPlayer ready"));
} else {
// If the SD card is missing or wiring is wrong, we don't crash —
// we just fall back to buzzer-only alerts.
[Link](F("DFPlayer FAILED - check SD card & wiring. Buzzer-only mode."));
Flood Monitoring System - flood_monitor_explained.ino Page 3
}
// Only bother connecting to Wi-Fi if cloud upload is enabled.
if (ENABLE_CLOUD) connectWiFi();
[Link](F("Flood monitoring system is running."));
}
/* ===========================================================================
* loop() — runs forever, as fast as the chip can go
* Notice there is NO delay() in here. Everything is timed with millis()
* so all three jobs (sensing, alerting, uploading) run "at the same time".
* ======================================================================== */
void loop() {
unsigned long now = millis(); // current time-since-boot in milliseconds
// JOB 1: every 2 seconds, read the sensors and update the state machine.
if (now - lastRead >= READ_INTERVAL) {
lastRead = now;
readSensors();
evaluateState();
logStatus();
}
// JOB 2: continuously run the LED/buzzer pattern for the current state.
runOutputs(now);
// JOB 3: every 30 seconds, send the readings to ThingSpeak (if enabled).
if (ENABLE_CLOUD && now - lastUpload >= UPLOAD_INTERVAL) {
lastUpload = now;
uploadThingSpeak();
}
}
/* ===========================================================================
* SENSOR FUNCTIONS
* ======================================================================== */
/* Reads the ultrasonic sensor and returns the distance in centimetres.
*
* HOW AN ULTRASONIC SENSOR WORKS:
* We send a 10-microsecond pulse on TRIG. The sensor fires a burst of
* sound (too high-pitched for humans to hear) which bounces off the
* water/floor and comes back. The ECHO pin stays HIGH for exactly as long
* as the sound took to make the round trip. Since sound travels at about
* 343 m/s (0.0343 cm per microsecond), the distance is:
* distance = (echo_time * 0.0343) / 2
* We divide by 2 because the sound travelled there AND back.
*
* We take THREE readings and use the MIDDLE one ("median"). This throws
* away occasional garbage readings caused by splashes or rain noise —
* one bad reading can't fool a median, but it would ruin an average. */
float readUltrasonicCm() {
float r[3]; // an array to hold the 3 readings
for (int i = 0; i < 3; i++) {
digitalWrite(PIN_TRIG, LOW); delayMicroseconds(2); // ensure clean start
digitalWrite(PIN_TRIG, HIGH); delayMicroseconds(10); // the 10 us trigger
digitalWrite(PIN_TRIG, LOW);
// pulseIn measures how long ECHO stays HIGH, giving up after 30 ms
// (30 ms = about 5 m, beyond the sensor's range) so we never hang forever.
unsigned long dur = pulseIn(PIN_ECHO, HIGH, 30000);
r[i] = (dur == 0) ? -1 : dur * 0.0343 / 2.0; // -1 means "no echo received"
delay(40); // small pause so echoes from one ping don't pollute the next
}
// Sort the 3 numbers smallest-to-largest (a tiny "bubble sort")...
for (int i = 0; i < 2; i++)
for (int j = i + 1; j < 3; j++)
if (r[j] < r[i]) { float t = r[i]; r[i] = r[j]; r[j] = t; }
return r[1]; // ...and return the middle one: the median.
}
Flood Monitoring System - flood_monitor_explained.ino Page 4
/* Reads all three sensors and stores the results in the global variables. */
void readSensors() {
float dist = readUltrasonicCm();
// Only accept the reading if it makes physical sense (a positive number
// not wildly larger than the mounting height). Otherwise we KEEP the
// previous good reading — during a flood, a broken sensor must not make
// the system think the water has vanished.
if (dist > 0 && dist <= MOUNT_HEIGHT_CM + 50) {
waterLevelCm = MOUNT_HEIGHT_CM - dist; // convert distance to depth
if (waterLevelCm < 0) waterLevelCm = 0; // can't have negative water
}
// FC-37: its onboard comparator pulls the digital pin LOW when the
// board detects water. Sensitivity is set with the small blue
// potentiometer screw on its driver board.
rainDetected = (digitalRead(PIN_RAIN) == LOW);
// analogRead returns 0-1023. For a capacitive soil sensor:
// dry soil ≈ 700-800, soaked soil/water ≈ 300-400.
soilRaw = analogRead(PIN_SOIL);
}
/* ===========================================================================
* THE BRAIN — deciding which state we should be in
* ======================================================================== */
void evaluateState() {
// Rule zero: once latched in CRITICAL, we never leave. Full stop.
if (criticalLatched) { state = STATE_CRITICAL; return; }
SystemState next = state; // assume we stay put unless a rule says otherwise
bool soilSaturated = (soilRaw < SOIL_WET_RAW);
switch (state) {
case STATE_NORMAL:
// From calm, we can jump straight to ANY higher stage — a flash
// flood won't politely pass through each stage in order.
if (waterLevelCm >= LEVEL_CRITICAL_CM) next = STATE_CRITICAL;
else if (waterLevelCm >= LEVEL_WARNING_CM) next = STATE_WARNING;
else if (rainDetected || soilSaturated) next = STATE_RAIN;
break;
case STATE_RAIN:
if (waterLevelCm >= LEVEL_CRITICAL_CM) next = STATE_CRITICAL;
else if (waterLevelCm >= LEVEL_WARNING_CM) next = STATE_WARNING;
// Only relax back to NORMAL when BOTH the rain has stopped AND the
// soil has drained — saturated ground can still flood after rain ends.
else if (!rainDetected && !soilSaturated) next = STATE_NORMAL;
break;
case STATE_WARNING:
if (waterLevelCm >= LEVEL_CRITICAL_CM) next = STATE_CRITICAL;
// Note the "- HYSTERESIS_CM": the water must fall a clear 3 cm
// below the warning line before we stand down. Without this, water
// sloshing at exactly 10.0 cm would switch the alarm on and off
// every two seconds.
else if (waterLevelCm < LEVEL_WARNING_CM - HYSTERESIS_CM)
next = (rainDetected || soilSaturated) ? STATE_RAIN : STATE_NORMAL;
break;
case STATE_CRITICAL:
break; // handled by the latch at the top — nothing to decide
}
// If the state CHANGED this cycle, run the one-time actions (play the
// voice alert, flip relays). We only do these on the moment of change,
// otherwise the voice clip would restart every 2 seconds forever.
if (next != state) onStateChange(state, next);
Flood Monitoring System - flood_monitor_explained.ino Page 5
state = next;
}
/* One-time actions performed at the moment we ENTER a new state. */
void onStateChange(SystemState from, SystemState to) {
switch (to) {
case STATE_RAIN:
playVoice(TRACK_LIGHT_RAIN); // "Light rain detected" x3
break;
case STATE_WARNING:
playVoice(TRACK_FLOOD_WARN); // "Flood warning" x3
break;
case STATE_CRITICAL:
criticalLatched = true; // lock the door behind us
digitalWrite(PIN_RELAY_AC, RELAY_ON); // energize relay 1 ->
// NC contact opens ->
// AC POWER IS CUT
digitalWrite(PIN_SIREN, RELAY_ON); // external siren screams
playVoice(TRACK_EVACUATE); // "Evacuate now" x3
[Link](F(">>> CRITICAL: MAINS CUT, SIREN ON, SYSTEM LATCHED <<<"));
break;
case STATE_NORMAL:
// Stand down all alert hardware when returning to calm.
digitalWrite(PIN_SIREN, RELAY_OFF);
digitalWrite(PIN_BUZZER, LOW);
digitalWrite(PIN_LED, HIGH); // inverted LED: HIGH = off
break;
}
}
/* Plays one MP3 track, repeated VOICE_REPEATS (3) times.
* The delay(4000) waits for the clip to finish before replaying — set it
* a little longer than your longest recorded message. */
void playVoice(int track) {
if (!dfOk) return; // speaker not working? skip — buzzer still covers us
for (int i = 0; i < VOICE_REPEATS; i++) {
dfPlayer.playMp3Folder(track); // plays /mp3/000<track>.mp3 from SD card
delay(4000);
}
}
/* ===========================================================================
* LED + BUZZER PATTERNS — runs every single loop, no delays
* Each state gets a distinct rhythm so even a deaf-and-mute system
* (broken speaker) still communicates urgency visually and audibly.
* ======================================================================== */
void runOutputs(unsigned long now) {
switch (state) {
case STATE_NORMAL:
break; // everything off, nothing to do
case STATE_RAIN:
// Gentle heartbeat: LED blinks once per second, no buzzer.
if (now - lastBeep >= 1000) {
lastBeep = now;
beepOn = !beepOn; // flip true<->false
digitalWrite(PIN_LED, beepOn ? LOW : HIGH);
}
break;
case STATE_WARNING:
// Urgent: fast triple-speed blink, buzzer chirping in sync.
if (now - lastBeep >= 300) {
lastBeep = now;
beepOn = !beepOn;
Flood Monitoring System - flood_monitor_explained.ino Page 6
digitalWrite(PIN_LED, beepOn ? LOW : HIGH);
digitalWrite(PIN_BUZZER, beepOn ? HIGH : LOW);
}
break;
case STATE_CRITICAL:
// Maximum alarm: LED solid on, buzzer screaming continuously.
digitalWrite(PIN_LED, LOW);
digitalWrite(PIN_BUZZER, HIGH);
break;
}
}
/* ===========================================================================
* CLOUD FUNCTIONS (only used when ENABLE_CLOUD is true)
* ======================================================================== */
/* Connects to the Wi-Fi network, but gives up after 15 seconds rather than
* waiting forever — the flood alerts matter more than the internet. */
void connectWiFi() {
[Link](WIFI_STA); // STA = "station" = normal client mode
[Link](WIFI_SSID, WIFI_PASS);
[Link](F("Connecting to WiFi"));
unsigned long t0 = millis();
while ([Link]() != WL_CONNECTED && millis() - t0 < 15000) {
delay(500);
[Link]('.');
}
[Link]([Link]() == WL_CONNECTED
? F(" connected!")
: F(" FAILED - running offline."));
}
/* Sends the four readings to ThingSpeak using a plain HTTP GET request.
* On [Link], make a free channel with 4 fields:
* field1 = water level (cm), field2 = rain (0/1),
* field3 = soil raw value, field4 = system state (0-3) */
void uploadThingSpeak() {
if ([Link]() != WL_CONNECTED) { connectWiFi(); return; }
WiFiClient client;
if () return; // server unreachable
String url = String("/update?api_key=") + TS_API_KEY +
"&field1=" + String(waterLevelCm, 1) +
"&field2=" + String(rainDetected ? 1 : 0) +
"&field3=" + String(soilRaw) +
"&field4=" + String((int)state);
[Link](String("GET ") + url +
" HTTP/1.1\r\nHost: [Link]\r\nConnection: close\r\n\r\n");
[Link]();
}
/* ===========================================================================
* DEBUG PRINTOUT — open the Serial Monitor (115200 baud) to watch this.
* Essential during calibration: you'll see the soil values and water level
* change live as you test with a bucket of water.
* ======================================================================== */
void logStatus() {
[Link](F("Water level: ")); [Link](waterLevelCm, 1);
[Link](F(" cm | Rain: ")); [Link](rainDetected ? F("YES") : F("no"));
[Link](F(" | Soil raw: ")); [Link](soilRaw);
[Link](F(" | State: "));
switch (state) {
case STATE_NORMAL: [Link](F("NORMAL")); break;
case STATE_RAIN: [Link](F("RAIN")); break;
case STATE_WARNING: [Link](F("WARNING")); break;
case STATE_CRITICAL: [Link](F("CRITICAL")); break;
}
}
Flood Monitoring System - flood_monitor_explained.ino Page 7