Flood Monitoring & Auto Power Cut-Off System
MicroPython source code for NodeMCU ESP8266 — save to the board as [Link]
# ============================================================================
# FLOOD MONITORING & AUTOMATIC POWER CUT-OFF SYSTEM
# MicroPython version for NodeMCU ESP8266
# ============================================================================
#
# 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) detects when the ground is
# so soaked ("saturated") that it cannot absorb more 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. 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. Readings are uploaded to ThingSpeak over Wi-Fi for remote monitoring.
#
# HOW TO PUT THIS ON THE NODEMCU:
# -------------------------------
# 1. Download the ESP8266 MicroPython firmware (.bin) from [Link]
# 2. Flash it once using esptool:
# pip install esptool
# esptool --port COM3 erase_flash
# esptool --port COM3 write_flash 0 [Link]
# 3. Install the Thonny editor ([Link]), select interpreter
# "MicroPython (ESP8266)", open this file and save it to the board
# as: [Link]
# (a file named [Link] runs automatically every time the board
# powers on - exactly what we need)
#
# HOW TO READ THIS FILE:
# ----------------------
# Everything above "MAIN PROGRAM" at the bottom is either a setting,
# a pin definition, or a function (a named block of code). The actual
# program is the while-True loop at the very bottom, which runs forever.
# ============================================================================
# ---------------------------------------------------------------------------
# LIBRARIES (all of these are built into MicroPython - nothing to install)
# ---------------------------------------------------------------------------
from machine import Pin, ADC, time_pulse_us, disable_irq, enable_irq
import time # for delays and timing
import network # for connecting to Wi-Fi
import socket # for talking to the ThingSpeak server
# ---------------------------------------------------------------------------
# USER SETTINGS - the ONLY values you should need to change
# ---------------------------------------------------------------------------
ENABLE_CLOUD = False # True = upload readings to ThingSpeak
WIFI_SSID = "YourWiFi" # your Wi-Fi network name
WIFI_PASS = "YourPassword" # your Wi-Fi password
TS_API_KEY = "YOUR_KEY_HERE" # from [Link] (free account)
# How high above the FLOOR the ultrasonic sensor is mounted, in cm.
# MEASURE THIS WITH A TAPE RULE after mounting. If the sensor hangs
# 250 cm above the floor and measures 240 cm to the surface below,
Flood Monitoring System - [Link] (MicroPython) Page 1
# the water is 250 - 240 = 10 cm deep.
MOUNT_HEIGHT_CM = 250.0
# Water depths (measured from the floor) that trigger each stage:
LEVEL_WARNING_CM = 10.0 # Stage 2 fires at 10 cm of water
LEVEL_CRITICAL_CM = 25.0 # Stage 3 fires at 25 cm (set BELOW your sockets!)
# Hysteresis stops the alarm flip-flopping when water hovers exactly at a
# threshold (small waves). Once WARNING is on, the water must drop a clear
# 3 cm below the line before the system stands down.
HYSTERESIS_CM = 3.0
# Soil moisture threshold (raw ADC 0-1023; capacitive sensor reads LOWER
# when WET). Calibrate: watch the printed values with the probe dry
# (about 700-800) and dipped in water (about 300-400), pick the midpoint.
SOIL_WET_RAW = 450
# ---------------------------------------------------------------------------
# PIN MAP
# MicroPython uses the chip's real GPIO numbers, not the "D" labels printed
# on the NodeMCU board. The translation is:
# D0=16 D1=5 D2=4 D3=0 D4=2 D5=14 D6=12 D7=13 D8=15 A0=ADC(0)
# ---------------------------------------------------------------------------
trig = Pin(5, [Link]) # D1 -> HC-SR04 Trig
echo = Pin(4, [Link]) # D2 -> HC-SR04 Echo
# !!! through a 1k + 2k voltage
# divider - Echo outputs 5V, the
# ESP8266 survives only 3.3V !!!
rain = Pin(0, [Link], Pin.PULL_UP) # D3 -> FC-37 digital out (LOW=wet)
df_tx = Pin(12, [Link], value=1) # D6 -> DFPlayer RX (1k in series)
# idle state of a serial line is HIGH
relay_ac = Pin(13, [Link], value=1) # D7 -> relay ch1 (AC cut-off)
siren = Pin(16, [Link], value=1) # D0 -> relay ch2 (external siren)
buzzer = Pin(15, [Link], value=0) # D8 -> active buzzer
led = Pin(2, [Link], value=1) # D4 -> red LED (onboard LED shares
# this pin and is INVERTED: 0 = on)
soil = ADC(0) # A0 -> soil moisture analog out
# Most cheap relay boards are ACTIVE LOW: writing 0 switches the relay ON.
# If yours is the opposite, swap these two numbers.
RELAY_ON = 0
RELAY_OFF = 1
# ---------------------------------------------------------------------------
# THE STATE MACHINE
# The system is always in exactly ONE of four named states. Each cycle we
# look at the sensors and decide whether to move to a different state.
# This keeps the logic clean instead of a tangle of if-statements.
# ---------------------------------------------------------------------------
STATE_NORMAL = 0 # dry, calm, nothing happening
STATE_RAIN = 1 # rain detected OR soil saturated - early warning
STATE_WARNING = 2 # water is physically rising on the floor
STATE_CRITICAL = 3 # flood level - power cut, evacuate
STATE_NAMES = ("NORMAL", "RAIN", "WARNING", "CRITICAL")
state = STATE_NORMAL # we start by assuming it is 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.
critical_latched = False
# Voice tracks. The DFPlayer's SD card must contain a folder named "mp3"
# holding files named EXACTLY: 0001.mp3, 0002.mp3, 0003.mp3
TRACK_LIGHT_RAIN = 1 # "Light rain detected, stay alert"
TRACK_FLOOD_WARN = 2 # "Flood warning, water level rising"
TRACK_EVACUATE = 3 # "Evacuate now, evacuate now"
VOICE_REPEATS = 3 # each message is played 3 times
Flood Monitoring System - [Link] (MicroPython) Page 2
# The latest sensor readings, shared between functions:
water_level_cm = 0.0
rain_detected = False
soil_raw = 1024 # 1024 = totally dry
# ---------------------------------------------------------------------------
# DFPLAYER MINI DRIVER (written by hand)
#
# MicroPython has no DFPlayer library, but the module is controlled by
# simple 10-byte messages sent over a 9600-baud serial line. The frame is:
#
# byte 0: 0x7E start marker
# byte 1: 0xFF version
# byte 2: 0x06 length of the middle section
# byte 3: CMD the command (0x06 = set volume, 0x12 = play /mp3 track)
# byte 4: 0x00 "no reply needed"
# byte 5: ARG high byte \ the command's parameter,
# byte 6: ARG low byte / e.g. track number or volume
# byte 7: CHK high byte \ checksum = 0 minus the sum
# byte 8: CHK low byte / of bytes 1 to 6
# byte 9: 0xEF end marker
#
# The ESP8266's only full serial port is busy talking to the computer
# (that is how Thonny shows our print() messages), so instead we
# "bit-bang" the signal: we switch the TX pin high and low ourselves
# with exact 104-microsecond timing, which IS a 9600-baud serial signal.
# We only ever SEND to the DFPlayer, never listen, so this works fine.
# ---------------------------------------------------------------------------
BIT_US = 104 # one bit at 9600 baud lasts 1 / 9600 s = 104 microseconds
def _send_byte(b):
"""Send one byte as a software-serial frame: start bit, 8 data bits
(least-significant first), stop bit. Interrupts are paused so Wi-Fi
background work cannot stretch our bit timing."""
irq = disable_irq()
df_tx.value(0) # start bit (line drops LOW)
time.sleep_us(BIT_US)
for i in range(8): # 8 data bits, LSB first
df_tx.value((b >> i) & 1)
time.sleep_us(BIT_US)
df_tx.value(1) # stop bit (line returns HIGH)
time.sleep_us(BIT_US)
enable_irq(irq)
def df_command(cmd, arg):
"""Build a 10-byte DFPlayer frame for (cmd, arg) and send it."""
high = (arg >> 8) & 0xFF # split arg into two bytes
low = arg & 0xFF
checksum = 0 - (0xFF + 0x06 + cmd + 0x00 + high + low)
checksum &= 0xFFFF # keep it inside 16 bits
frame = bytes([0x7E, 0xFF, 0x06, cmd, 0x00, high, low,
(checksum >> 8) & 0xFF, checksum & 0xFF, 0xEF])
for b in frame:
_send_byte(b)
time.sleep_ms(30) # give the module a breath
def df_set_volume(vol):
df_command(0x06, vol) # 0x06 = set volume, range 0-30
def df_play_mp3(track):
df_command(0x12, track) # 0x12 = play /mp3/000N.mp3 from SD card
def play_voice(track):
"""Play one voice message, repeated VOICE_REPEATS (3) times.
The 4-second pause lets each clip finish - make it a little longer
than your longest recording."""
for _ in range(VOICE_REPEATS):
df_play_mp3(track)
[Link](4)
Flood Monitoring System - [Link] (MicroPython) Page 3
# ---------------------------------------------------------------------------
# SENSOR FUNCTIONS
# ---------------------------------------------------------------------------
def read_ultrasonic_cm():
"""Measure distance with the HC-SR04 and return centimetres.
HOW IT WORKS: we pulse Trig HIGH for 10 microseconds. The sensor fires
a burst of sound (too high-pitched to hear) that bounces off the water
or floor and returns. The Echo pin stays HIGH for exactly the round-trip
time. Sound travels 0.0343 cm per microsecond, so:
distance = (echo_time * 0.0343) / 2 (divide by 2: there AND back)
We take THREE readings and keep the MIDDLE one (the median). A single
splash can ruin an average, but it cannot fool a median."""
readings = []
for _ in range(3):
[Link](0)
time.sleep_us(2)
[Link](1)
time.sleep_us(10) # the 10-microsecond trigger pulse
[Link](0)
# time_pulse_us measures how long Echo stays HIGH; we give up
# after 30000 us (about 5 m, beyond the sensor's range) so a
# missing echo can never freeze the program. It returns a
# negative number on timeout.
duration = time_pulse_us(echo, 1, 30000)
if duration > 0:
[Link](duration * 0.0343 / 2)
time.sleep_ms(40) # let old echoes die before the next ping
if not readings:
return -1 # -1 means "no valid echo this round"
[Link]()
return readings[len(readings) // 2] # the median
def read_sensors():
"""Read all sensors and update the shared variables."""
global water_level_cm, rain_detected, soil_raw
dist = read_ultrasonic_cm()
# Only accept the reading if it makes physical sense. Otherwise KEEP
# the previous good value - during a flood, a glitching sensor must
# not make the system believe the water has vanished.
if 0 < dist <= MOUNT_HEIGHT_CM + 50:
water_level_cm = max(0.0, MOUNT_HEIGHT_CM - dist)
# FC-37 pulls its digital pin LOW when the plate is wet. Sensitivity
# is set with the small potentiometer screw on its driver board.
rain_detected = ([Link]() == 0)
# ADC returns 0-1023. Capacitive soil sensor: dry soil reads about
# 700-800, soaked soil/water about 300-400.
soil_raw = [Link]()
# ---------------------------------------------------------------------------
# THE BRAIN - deciding which state we should be in
# ---------------------------------------------------------------------------
def evaluate_state():
global state, critical_latched
# Rule zero: once latched in CRITICAL we never leave. Full stop.
if critical_latched:
state = STATE_CRITICAL
return
nxt = state # assume we stay unless a rule fires
soil_saturated = (soil_raw < SOIL_WET_RAW)
if state == STATE_NORMAL:
# From calm we may jump straight to ANY higher stage - a flash
# flood will not politely pass through each stage in order.
Flood Monitoring System - [Link] (MicroPython) Page 4
if water_level_cm >= LEVEL_CRITICAL_CM:
nxt = STATE_CRITICAL
elif water_level_cm >= LEVEL_WARNING_CM:
nxt = STATE_WARNING
elif rain_detected or soil_saturated:
nxt = STATE_RAIN
elif state == STATE_RAIN:
if water_level_cm >= LEVEL_CRITICAL_CM:
nxt = STATE_CRITICAL
elif water_level_cm >= LEVEL_WARNING_CM:
nxt = STATE_WARNING
# Only relax to NORMAL when BOTH rain has stopped AND the soil
# has drained - saturated ground can still flood after rain ends.
elif not rain_detected and not soil_saturated:
nxt = STATE_NORMAL
elif state == STATE_WARNING:
if water_level_cm >= LEVEL_CRITICAL_CM:
nxt = STATE_CRITICAL
# Note the "- HYSTERESIS_CM": water must fall a clear 3 cm below
# the warning line before standing down, or sloshing water at
# exactly 10.0 cm would switch the alarm on/off every cycle.
elif water_level_cm < LEVEL_WARNING_CM - HYSTERESIS_CM:
nxt = STATE_RAIN if (rain_detected or soil_saturated) else STATE_NORMAL
# If the state CHANGED, run the one-time entry actions (voice alert,
# relays). Only on the moment of change - otherwise the voice clip
# would restart every cycle forever.
if nxt != state:
on_state_change(nxt)
state = nxt
def on_state_change(to):
"""One-time actions performed at the moment we ENTER a new state."""
global critical_latched
if to == STATE_RAIN:
play_voice(TRACK_LIGHT_RAIN) # "Light rain detected" x3
elif to == STATE_WARNING:
play_voice(TRACK_FLOOD_WARN) # "Flood warning" x3
elif to == STATE_CRITICAL:
critical_latched = True # lock the door behind us
relay_ac.value(RELAY_ON) # energize relay 1 ->
# NC contact opens ->
# AC POWER IS CUT
[Link](RELAY_ON) # external siren screams
play_voice(TRACK_EVACUATE) # "Evacuate now" x3
print(">>> CRITICAL: POWER CUT, SIREN ON, SYSTEM LATCHED <<<")
elif to == STATE_NORMAL:
# Stand down all alert hardware when returning to calm.
[Link](RELAY_OFF)
[Link](0)
[Link](1) # inverted LED: 1 = off
# ---------------------------------------------------------------------------
# LED + BUZZER PATTERNS - called every cycle, no blocking delays
# Each state gets a distinct rhythm, so even with a broken speaker the
# system still communicates urgency by sight and sound.
# ---------------------------------------------------------------------------
beep_on = False
last_beep = 0
def run_outputs(now_ms):
global beep_on, last_beep
if state == STATE_RAIN:
Flood Monitoring System - [Link] (MicroPython) Page 5
# Gentle heartbeat: LED blinks once per second, no buzzer.
if time.ticks_diff(now_ms, last_beep) >= 1000:
last_beep = now_ms
beep_on = not beep_on
[Link](0 if beep_on else 1)
elif state == STATE_WARNING:
# Urgent: fast blink, buzzer chirping in sync.
if time.ticks_diff(now_ms, last_beep) >= 300:
last_beep = now_ms
beep_on = not beep_on
[Link](0 if beep_on else 1)
[Link](1 if beep_on else 0)
elif state == STATE_CRITICAL:
# Maximum alarm: LED solid on, buzzer screaming continuously.
[Link](0)
[Link](1)
# ---------------------------------------------------------------------------
# CLOUD FUNCTIONS (only used when ENABLE_CLOUD is True)
# ---------------------------------------------------------------------------
def connect_wifi():
"""Join the Wi-Fi network, giving up after 15 seconds - the flood
alerts matter more than the internet."""
wlan = [Link](network.STA_IF) # STA = normal client mode
[Link](True)
if [Link]():
return wlan
print("Connecting to WiFi", end="")
[Link](WIFI_SSID, WIFI_PASS)
t0 = time.ticks_ms()
while not [Link]() and time.ticks_diff(time.ticks_ms(), t0) < 15000:
time.sleep_ms(500)
print(".", end="")
print(" connected!" if [Link]() else " FAILED - running offline.")
return wlan
def upload_thingspeak():
"""Send the four readings to ThingSpeak with 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)"""
try:
wlan = connect_wifi()
if not [Link]():
return
addr = [Link]("[Link]", 80)[0][-1]
s = [Link]()
[Link](5)
[Link](addr)
path = ("/update?api_key=" + TS_API_KEY +
"&field1=" + str(round(water_level_cm, 1)) +
"&field2=" + ("1" if rain_detected else "0") +
"&field3=" + str(soil_raw) +
"&field4=" + str(state))
[Link](b"GET " + [Link]() +
b" HTTP/1.1\r\nHost: [Link]\r\nConnection: close\r\n\r\n")
[Link]()
except Exception as e:
# Any network hiccup is printed and ignored - it must NEVER be
# allowed to crash the safety system.
print("Upload failed:", e)
# ---------------------------------------------------------------------------
# DEBUG PRINTOUT - watch this live in Thonny's shell. Essential during
# calibration: you will see soil values and water level change as you
# test with a cup and a bucket of water.
# ---------------------------------------------------------------------------
def log_status():
Flood Monitoring System - [Link] (MicroPython) Page 6
print("Water level: %.1f cm | Rain: %s | Soil raw: %d | State: %s" %
(water_level_cm, "YES" if rain_detected else "no",
soil_raw, STATE_NAMES[state]))
# ===========================================================================
# MAIN PROGRAM - runs forever
# Three jobs share the loop without ever blocking each other:
# every 2 s read sensors + update the state machine
# every cycle run the LED/buzzer pattern for the current state
# every 30 s upload to the cloud (if enabled)
# We time things with ticks_ms() instead of long sleeps, so the buzzer
# and LED patterns stay smooth.
# ===========================================================================
print("Flood monitoring system starting...")
df_set_volume(28) # speaker volume, 0 (mute) to 30 (max)
if ENABLE_CLOUD:
connect_wifi()
print("Running.")
last_read = 0
last_upload = 0
READ_INTERVAL_MS = 2000
UPLOAD_INTERVAL_MS = 30000
while True:
now = time.ticks_ms() # milliseconds since power-on
# JOB 1: sensors + state machine, every 2 seconds
if time.ticks_diff(now, last_read) >= READ_INTERVAL_MS:
last_read = now
read_sensors()
evaluate_state()
log_status()
# JOB 2: alert patterns, every single cycle
run_outputs(now)
# JOB 3: cloud upload, every 30 seconds
if ENABLE_CLOUD and time.ticks_diff(now, last_upload) >= UPLOAD_INTERVAL_MS:
last_upload = now
upload_thingspeak()
time.sleep_ms(20) # tiny rest - keeps Wi-Fi happy and
# stops the loop hogging the chip
Flood Monitoring System - [Link] (MicroPython) Page 7