Leak detector with shutoff valve on an Arduino Uno
A teaching model: a YF-S201 flow sensor and an EEPROM totalizer watch for water that never stops or a trickle nobody is using, then a solenoid valve closes the main until you press reopen. The whole build, an Arduino Uno and 12 more parts, runs here in your browser on the firmware below; open it in the editor to change the wiring or the code and run it again.
Teaching? Assign it to your class in one click.
Intermediate About 40 minutes to build and run. Runs in your browser. Free, and no account needed.
The parts retail for about $41. On Mokxi they're free.
What you'll learn
- Sensors
- Motors
- Making sound with a buzzer
- Choosing and reading a resistor
- How an LED lights and why it needs a resistor
- Difficulty: Intermediate
- Time: About 40 minutes
- Board: Arduino Uno R3
- Parts: 13
How it works
A teaching model: a YF-S201 flow sensor and an EEPROM totalizer watch for water that never stops or a trickle nobody is using, then a solenoid valve closes the main until you press reopen.
Arduino Uno R3 runs the sketch below. The circuit around it: Water flow sensor, YF-S201, Solenoid, Bench power supply, MOSFET, Diode, Buzzer. pinMode sets FLOW_PIN as an input with its pull-up on. digitalWrite drives VALVE LOW. digitalRead reads whether b.pin is HIGH or LOW. Serial.begin opens the serial port at 115200 baud. millis reads the time since power-up, so the sketch can time things without stopping.
Parts list
13 parts, plus the jumper wires and a bench instrument. The parts retail for about $41; on Mokxi they're free, and so are the instruments.
- 1 × Arduino Uno R3In Starter Kit, Claw Machine Kit, Vending Machine Kit.
- 1 × Full-size breadboard
- 1 × Water flow sensor, YF-S201
- 1 × Solenoid
- 1 × Bench power supplyIn Pinball Table Kit.
- 1 × MOSFETIn Pinball Table Kit.
- 1 × DiodeIn Pinball Table Kit.
- 1 × BuzzerIn Starter Kit, Rocket Flight Computer Kit.
- 1 × Resistor, 220 ΩIn Starter Kit, Claw Machine Kit, Halloween Prop Kit.
- 1 × LED, redIn Starter Kit, Halloween Prop Kit, Logic Lab Kit.
- 2 × PushbuttonIn Starter Kit, Christmas Lights Kit, Logic Lab Kit.
- 2 × Slide switchIn Logic Lab Kit, Spaceship Panel Kit.
Wiring table
Every connection, pin by pin, read from the circuit itself. Each row joins the pin on the left to the pin on the right, by a jumper wire or a breadboard strip.
| Part and pin | Connects to |
|---|---|
| Resistor, 220 Ω pin 1 | Arduino Uno R3 pin 12 |
| MOSFET pin G | Arduino Uno R3 pin 9 |
| Buzzer pin 1 | Arduino Uno R3 pin 8 |
| Slide switch (2) pin 2 | Arduino Uno R3 pin 6 |
| Slide switch (1) pin 2 | Arduino Uno R3 pin 5 |
| Pushbutton (2) pin 1a | Arduino Uno R3 pin 4 |
| Pushbutton (1) pin 1a | Arduino Uno R3 pin 3 |
| Water flow sensor, YF-S201 pin SIG | Arduino Uno R3 pin 2 |
| Water flow sensor, YF-S201 pin VCC | Arduino Uno R3 pin 5V |
| Water flow sensor, YF-S201 pin GND | Arduino Uno R3 pin GND |
| Bench power supply pin V- | Arduino Uno R3 pin GND |
| MOSFET pin S | Arduino Uno R3 pin GND |
| Buzzer pin 2 | Arduino Uno R3 pin GND |
| LED, red pin C | Arduino Uno R3 pin GND |
| Pushbutton (1) pin 2a | Arduino Uno R3 pin GND |
| Pushbutton (2) pin 2a | Arduino Uno R3 pin GND |
| Slide switch (1) pin 1 | Arduino Uno R3 pin GND |
| Slide switch (2) pin 1 | Arduino Uno R3 pin GND |
| Bench power supply pin V+ | Solenoid pin 1 |
| Diode pin C | Solenoid pin 1 |
| MOSFET pin D | Solenoid pin 2 |
| Diode pin A | Solenoid pin 2 |
| LED, red pin A | Resistor, 220 Ω pin 2 |
The code
The firmware exactly as the editor opens it. Change a line there and press Run: it compiles in the browser.
// Leak detector: a YF-S201 flow sensor, a totalizer that survives a reset, and
// a solenoid valve that closes the main when a leak is confirmed.
//
// A TEACHING MODEL with low-voltage stand-ins. Do not wire it into a building's
// water main, and do not count on it to protect a house. A real shutoff is a
// listed valve with its own controller and a leak sensor that is certified for
// the job.
//
// pin 2 the flow sensor's SIG (interrupt 0); VCC and GND from the 5 V rails
// pin 3 REOPEN button to GND (reopens the valve after a leak)
// pin 4 DAILY button to GND (zeroes today's total)
// pin 5 UNITS slide switch to GND: down is liters, up is gallons
// pin 6 USAGE slide switch to GND: down means someone is home and using
// water, up means quiet hours or nobody home
// pin 8 an active buzzer (its other leg to GND), beeped on and off
// pin 9 the valve driver: gate of an IRLZ44N on the 12 V valve, with a
// 1N4007 across the coil to catch the kick when it lets go
// pin 12 the red alarm LED, through 220 ohms
//
// The sensor. The wheel in a YF-S201 pulses SIG about 7.5 times a second for
// each liter a minute, so 450 pulses make a liter. That is the datasheet's
// typical figure, the same one the water meter uses. In the real world the
// K-factor varies from unit to unit and with the flow rate (a unit reads off at
// the very low and very high ends, and a trickle may not turn the wheel at
// all), so a real install calibrates against a measured bucket of water.
//
// Time scale. A leak is a slow thing and a simulator is not. The two limits
// below are written in seconds and are scaled 30 to 1, so the simulator can
// run them in a moment:
//
// LONG_FLOW_S 20 s here stands for 10 minutes of flow that never lets up
// TRICKLE_S 40 s here stands for 20 minutes of a small flow that never
// stops while nobody is using water
//
// The two rules.
// 1. Continuous flow: more than a trickle, without a gap of GAP_S seconds,
// for LONG_FLOW_S. That is a burst line, a stuck toilet fill valve or a
// hose left running. (A sprinkler on a timer would trip it too.)
// 2. Slow leak: a flow of 0.5 L/min or less that never stops for TRICKLE_S
// while the USAGE switch says nobody is using water. A running toilet or
// a dripping fixture looks like this. Flow while someone is home is use,
// not a leak, and clears the count.
// Half way to either limit the state is WATCH (the LED blinks). At the limit
// it is LEAK: the valve closes, the LED lights and the buzzer beeps.
//
// The valve is a normally closed solenoid, held OPEN by 12 V on the coil. A
// leak (or a dead supply, or a dead Arduino) lets go of it and it shuts: the
// safe side to fail on. The leak is remembered in EEPROM, so a reset does not
// reopen the main by itself; only the REOPEN button does.
//
// The totals. Pulses are the only thing counted, so liters and gallons are
// worked out when they are shown (450 pulses a liter, 1703 a gallon). The
// total and today's total are written to EEPROM every 5 liters, and when a
// leak is confirmed or cleared. A cell takes about 100,000 writes, so the
// sketch saves rarely; a longer-lived design would also spread the writes
// with a wear-leveling library. Up to 5 liters can be lost in a power cut.
#include <EEPROM.h>
const int FLOW_PIN = 2;
const int REOPEN_BUTTON = 3;
const int DAILY_BUTTON = 4;
const int UNITS_SWITCH = 5;
const int USAGE_SWITCH = 6;
const int BUZZER = 8;
const int VALVE = 9;
const int LED = 12;
const unsigned long PULSES_PER_LITER = 450;
const unsigned long PULSES_PER_GALLON = 1703; // 450 x 3.785
const unsigned long LONG_FLOW_S = 20; // stands for 10 minutes
const unsigned long TRICKLE_S = 40; // stands for 20 minutes
const unsigned long GAP_S = 3; // quiet seconds that end a run of flow
const unsigned long TRICKLE_MAX = 5; // tenths of a liter a minute (0.5)
const unsigned long SAVE_EVERY = 5 * PULSES_PER_LITER;
const byte MAGIC = 0x4C;
struct Saved {
byte magic;
unsigned long total; // pulses, for good
unsigned long today; // pulses since the DAILY button
byte leak; // 1 while a leak is latched
};
volatile unsigned long pulses = 0;
unsigned long counted = 0;
unsigned long total = 0;
unsigned long today = 0;
unsigned long savedTotal = 0;
unsigned long lastSecond = 0;
unsigned long ring[5];
byte ringAt = 0;
unsigned long quietS = 99;
unsigned long contS = 0;
unsigned long trickleS = 0;
bool leak = false;
const char *state = "OK";
struct Button {
int pin;
bool last;
unsigned long changed;
};
Button reopenButton = {REOPEN_BUTTON, false, 0};
Button dailyButton = {DAILY_BUTTON, false, 0};
void onPulse() {
pulses++;
}
// True once per press, after the contact has held for 40 ms.
bool pressed(Button &b) {
bool down = digitalRead(b.pin) == LOW;
if (down != b.last) {
b.last = down;
b.changed = millis();
return false;
}
if (down && b.changed != 0 && millis() - b.changed >= 40) {
b.changed = 0;
return true;
}
return false;
}
void save() {
Saved s = {MAGIC, total, today, (byte)(leak ? 1 : 0)};
EEPROM.put(0, s);
savedTotal = total;
Serial.print("saved total ");
Serial.print(total);
Serial.print(" pulses, today ");
Serial.print(today);
Serial.print(", leak ");
Serial.println(leak ? 1 : 0);
}
void printTenths(unsigned long t) {
Serial.print(t / 10);
Serial.print('.');
Serial.print(t % 10);
}
void closeValve(const char *why) {
leak = true;
digitalWrite(VALVE, LOW);
Serial.print("LEAK: ");
Serial.print(why);
Serial.println(", valve closed");
save();
}
void setup() {
Serial.begin(115200);
Serial.println("Mokxi Arduino Uno: leak detector");
pinMode(FLOW_PIN, INPUT_PULLUP);
pinMode(REOPEN_BUTTON, INPUT_PULLUP);
pinMode(DAILY_BUTTON, INPUT_PULLUP);
pinMode(UNITS_SWITCH, INPUT_PULLUP);
pinMode(USAGE_SWITCH, INPUT_PULLUP);
pinMode(BUZZER, OUTPUT);
pinMode(VALVE, OUTPUT);
pinMode(LED, OUTPUT);
Saved s;
EEPROM.get(0, s);
if (s.magic == MAGIC) {
total = s.total;
today = s.today;
leak = s.leak == 1;
}
savedTotal = total;
// Open the main only if no leak is waiting to be cleared.
digitalWrite(VALVE, leak ? LOW : HIGH);
Serial.print("boot: total ");
Serial.print(total);
Serial.print(" pulses, today ");
Serial.print(today);
Serial.println(leak ? ", leak latched, valve stays closed" : ", valve open");
attachInterrupt(digitalPinToInterrupt(FLOW_PIN), onPulse, FALLING);
lastSecond = millis();
}
// Once a second: count, judge and report.
void tick() {
noInterrupts();
unsigned long all = pulses;
interrupts();
unsigned long delta = all - counted;
counted = all;
total += delta;
today += delta;
ring[ringAt] = delta;
ringAt = (ringAt + 1) % 5;
unsigned long five = 0;
for (int i = 0; i < 5; i++) five += ring[i];
bool gallons = digitalRead(UNITS_SWITCH) == HIGH;
bool using_water = digitalRead(USAGE_SWITCH) == LOW;
unsigned long perUnit = gallons ? PULSES_PER_GALLON : PULSES_PER_LITER;
unsigned long perMinute10 = (five * 120 + perUnit / 2) / perUnit; // in display units
unsigned long liters10 = (five * 120 + PULSES_PER_LITER / 2) / PULSES_PER_LITER;
quietS = delta > 0 ? 0 : quietS + 1;
bool flowing = quietS < GAP_S;
if (!flowing) {
contS = 0;
trickleS = 0;
} else if (liters10 > TRICKLE_MAX) {
// The seconds after the water stops (while the average still reads high)
// are not flow, so only a second with pulses adds to the run.
if (delta > 0) contS++;
trickleS = 0;
} else {
contS = 0;
trickleS = using_water ? 0 : trickleS + 1;
}
if (!leak) {
if (contS >= LONG_FLOW_S) closeValve("flow never stopped");
else if (trickleS >= TRICKLE_S) closeValve("slow leak, nobody using water");
}
if (leak) state = "LEAK";
else if (contS * 2 >= LONG_FLOW_S || trickleS * 2 >= TRICKLE_S) state = "WATCH";
else state = "OK";
if (!leak && total - savedTotal >= SAVE_EVERY) save();
Serial.print("flow ");
printTenths(perMinute10);
Serial.print(gallons ? " gal/min total " : " L/min total ");
printTenths(total * 10 / perUnit);
Serial.print(gallons ? " gal state " : " L state ");
Serial.print(state);
Serial.print(" today ");
printTenths(today * 10 / perUnit);
Serial.print(gallons ? " gal valve " : " L valve ");
Serial.println(leak ? "closed" : "open");
}
void loop() {
if (millis() - lastSecond >= 1000) {
lastSecond += 1000;
tick();
}
if (pressed(reopenButton) && leak) {
leak = false;
contS = 0;
trickleS = 0;
quietS = 99;
digitalWrite(VALVE, HIGH);
Serial.println("valve reopened");
save();
}
if (pressed(dailyButton)) {
today = 0;
Serial.println("daily total reset");
save();
}
// LEAK: solid LED and a buzzer beeping twice a second. WATCH: a slow blink.
bool beat = (millis() / 500) % 2;
if (leak) {
digitalWrite(LED, HIGH);
digitalWrite(BUZZER, beat ? HIGH : LOW);
} else {
digitalWrite(LED, state[0] == 'W' && beat ? HIGH : LOW);
digitalWrite(BUZZER, LOW);
}
}
Code walkthrough
The key functions of the sketch, and what each one does.
setup sketch.ino
Runs once when the board starts. It gets the pins and libraries ready.
void setup() {
Serial.begin(115200);
Serial.println("Mokxi Arduino Uno: leak detector");
pinMode(FLOW_PIN, INPUT_PULLUP);
pinMode(REOPEN_BUTTON, INPUT_PULLUP);
pinMode(DAILY_BUTTON, INPUT_PULLUP);
pinMode(UNITS_SWITCH, INPUT_PULLUP);
pinMode(USAGE_SWITCH, INPUT_PULLUP);
pinMode(BUZZER, OUTPUT);
pinMode(VALVE, OUTPUT);
pinMode(LED, OUTPUT);
Saved s;
EEPROM.get(0, s);
if (s.magic == MAGIC) {
total = s.total;
today = s.today;
leak = s.leak == 1;
}
savedTotal = total;
// Open the main only if no leak is waiting to be cleared.
digitalWrite(VALVE, leak ? LOW : HIGH);
Serial.print("boot: total ");
Serial.print(total);
Serial.print(" pulses, today ");
Serial.print(today);
Serial.println(leak ? ", leak latched, valve stays closed" : ", valve open");
// ... 3 more lines in the editorloop sketch.ino
Runs over and over for as long as the board has power. This is where the project does its work.
void loop() {
if (millis() - lastSecond >= 1000) {
lastSecond += 1000;
tick();
}
if (pressed(reopenButton) && leak) {
leak = false;
contS = 0;
trickleS = 0;
quietS = 99;
digitalWrite(VALVE, HIGH);
Serial.println("valve reopened");
save();
}
if (pressed(dailyButton)) {
today = 0;
Serial.println("daily total reset");
save();
}
// LEAK: solid LED and a buzzer beeping twice a second. WATCH: a slow blink.
bool beat = (millis() / 500) % 2;
if (leak) {
digitalWrite(LED, HIGH);
digitalWrite(BUZZER, beat ? HIGH : LOW);
} else {
digitalWrite(LED, state[0] == 'W' && beat ? HIGH : LOW);
// ... 3 more lines in the editorTroubleshooting
The usual mistakes with this circuit, and the fix for each.
- The count is zero or erratic. Water flow sensor
Connect red to 5 V, black to ground and yellow to an interrupt pin. Use a pull-up if the signal does not rise cleanly. Attach an interrupt on the rising edge and count pulses. - The flow rate looks wrong. Water flow sensor
For the YF-S201, the pulse frequency in Hz is about 7.5 times the flow in liters per minute. Count pulses for a full second, then divide by 7.5. Other models use a different factor. - The board resets, or the pin burns out, when the solenoid is wired straight to a pin. Solenoid
A solenoid draws far too much for a pin. Switch it with a MOSFET or transistor from its own supply (such as 12 V), with the grounds joined. - The transistor or the controller fails after some switching. Solenoid
A coil produces a voltage spike when current stops. Put a diode across the coil, cathode on the supply side, such as a 1N4007 or 1N5819, to absorb the kick. - The output voltage looks wrong or collapses under load because the current limit is set too low and the supply is in constant current mode (CC). Bench power supply
Set the voltage first, then set the current limit above what the circuit needs. Watch the CV and CC lamps. CV means the voltage is held, CC means the limit is working. - Too much voltage burns parts that expect 3.3 V or 5 V. Bench power supply
Set the voltage with the output off and nothing connected, check it, then connect. Start the current limit low for a new circuit, like 100 mA, and raise it if needed. - The load stays partly on or flickers when the pin is not driving, such as at boot or in input mode. MOSFET
A floating gate picks up noise. Add a pull-down of about 10 k from gate to source on an N-channel part, or pull a P-channel gate up to its source voltage. - The N-channel load is always on or never turns on. Source, gate, and drain legs were mixed up. MOSFET
Use the N-channel as a low-side switch: load to the supply, drain to the load, source to ground. Check your datasheet pinout, since 2N7000 and BS250 differ. Share the grounds.
Change it and keep it
Open it in the editor, change the circuit or the code, and keep your version in a free account.