A water sensor Arduino setup relies on measuring the electrical resistance between two exposed conductive traces. When liquid bridges the gap, the resistance drops, causing the analog voltage at the microcontroller pin to rise. While the concept is simple, the reality of building a reliable leak detector involves managing analog-to-digital converter (ADC) noise, floating pins, and the inevitable galvanic corrosion that destroys cheap sensors. This guide targets the Arduino Uno R3 (ATmega328P) and Arduino Nano v3, walking through the exact hardware, a noise-filtered codebase, and the specific failure modes that cause 90% of debugging headaches.

Build Profile: Difficulty 2/5 | Time: 45 minutes | Target Board: Arduino Uno R3 / Nano v3

Hardware Specs and Pin Mapping

Before wiring, you need to select the right sensor for your environment. The ubiquitous FC-37 module is fine for temporary prototypes, but its exposed copper tines will degrade rapidly in standing water. Here is how the common options compare in 2026.

Sensor Module Type Operating Voltage Output Lifespan in Standing Water Approx. Cost (2026)
Generic FC-37 Resistive (Bare PCB) 3.3V - 5V Analog / Digital (LM393) 2-4 weeks (Electrolysis) $1.50 - $2.50
Adafruit 1830 Resistive (ENIG Coated) 3.3V - 5V Analog 3-6 months $7.95
SlickPie / Generic I2C Rope Capacitive / Advanced 5V I2C Digital Years (No exposed metal) $18.00 - $25.00

Required Parts List

  • Microcontroller: Arduino Uno R3 or compatible ATmega328P board.
  • Sensor: FC-37 Water/Rain sensor module (includes the LM393 comparator daughterboard).
  • Resistor: 10kΩ (Brown-Black-Orange-Gold) for the analog pull-down.
  • Alert: 5V Active Piezo Buzzer (e.g., KY-012 module).
  • Wiring: 22 AWG solid core jumper wires.

Pin Mapping Table

Component Module Pin Arduino Uno Pin Notes
Water SensorVCC5VDo not use 3.3V; LM393 needs 5V for full swing.
Water SensorGNDGNDCommon ground required.
Water SensorA0 (Analog)A0Requires 10kΩ pull-down to GND.
Water SensorD0 (Digital)Not UsedUnused in analog smoothing build.
Piezo BuzzerVCC / SignalD8Use PWM pin if volume control is needed.
Piezo BuzzerGNDGNDShared ground rail.

Step-by-Step Wiring and Assembly

  1. Prepare the Pull-Down Resistor: Insert one leg of the 10kΩ resistor into the Arduino's GND rail and the other leg into the A0 pin row on your breadboard. This is non-negotiable. Without it, the high-impedance A0 pin will read atmospheric noise, causing false alarms. Read more on why this happens in SparkFun's guide on pull-up/pull-down resistors.
  2. Wire the Sensor Module: Connect the FC-37 VCC to the 5V rail and GND to the ground rail. Connect the A0 output pin to the breadboard row sharing the Arduino A0 pin and the 10kΩ resistor.
  3. Wire the Buzzer: Connect the active buzzer's positive pin to Arduino D8 and the negative pin to GND.
  4. Apply Conformal Coating (Crucial): Take clear nail polish or a dedicated acrylic conformal coating and paint the top edge of the FC-37 sensor board where the tines meet the PCB. Leave only the bottom 10mm of the tines exposed. This prevents water from wicking up into the connector pins and causing shorts.
  5. Verify Connections: Use a multimeter in continuity mode to verify that the sensor GND and Arduino GND share a common path, and that the 10kΩ resistor is correctly bridging A0 and GND.
Callout Tip: Never route the sensor wires parallel to AC mains cables. The 50/60Hz electromagnetic field will induce a voltage in the high-impedance analog wire, which the Arduino will interpret as a water leak.

Complete Arduino IDE Code with Error Handling

This sketch targets the Arduino Uno R3. It uses an Exponential Moving Average (EMA) to smooth out ADC jitter and includes diagnostic error handling to detect if the sensor becomes disconnected or shorts out. For a deeper look at how the ADC samples voltage, refer to the official Arduino analogRead documentation.

// Water Sensor Arduino Leak Detector with EMA Smoothing
// Target: Arduino Uno R3 / Nano v3 (ATmega328P)

#define SENSOR_PIN A0
#define BUZZER_PIN 8
#define STATUS_LED LED_BUILTIN

// Thresholds (10-bit ADC: 0-1023)
#define DRY_THRESHOLD 50      // Anything below 50 is considered dry
#define WET_THRESHOLD 400     // Alarm triggers above this value
#define DISCONNECT_THRESHOLD 15 // If pull-down is working, disconnected = near 0
#define SHORT_THRESHOLD 1015  // Sensor shorted to VCC

// EMA Smoothing Factor (0.0 to 1.0). Lower = smoother but slower response.
const float ALPHA = 0.15;
float smoothedValue = 0.0;

unsigned long lastPrintTime = 0;
bool alarmActive = false;

void setup() {
  Serial.begin(115200);
  pinMode(BUZZER_PIN, OUTPUT);
  pinMode(STATUS_LED, OUTPUT);
  pinMode(SENSOR_PIN, INPUT); // High impedance by default
  
  digitalWrite(BUZZER_PIN, LOW);
  digitalWrite(STATUS_LED, LOW);
  
  // Initialize smoothed value with a baseline reading
  smoothedValue = analogRead(SENSOR_PIN);
  Serial.println("System Initialized. Calibrating baseline...");
  delay(500);
}

void loop() {
  int rawValue = analogRead(SENSOR_PIN);
  
  // Apply Exponential Moving Average (EMA) filter
  smoothedValue = (ALPHA * rawValue) + ((1.0 - ALPHA) * smoothedValue);
  int currentLevel = (int)smoothedValue;
  
  // Error Handling & Diagnostics
  if (rawValue >= SHORT_THRESHOLD) {
    Serial.println("ERR: SENSOR_SHORT (Read: >1015). Check for water bridging VCC to A0.");
    triggerErrorBlink();
    return;
  }
  
  if (rawValue <= DISCONNECT_THRESHOLD && smoothedValue <= DISCONNECT_THRESHOLD) {
    // Note: A true disconnect with a pull-down reads 0. 
    // If you see random 300-800 values, your pull-down resistor is missing.
    Serial.println("WARN: SENSOR_DISCONNECT or FLOATING. Verify 10k pull-down to GND.");
  }

  // Leak Detection Logic
  if (currentLevel >= WET_THRESHOLD && !alarmActive) {
    alarmActive = true;
    digitalWrite(BUZZER_PIN, HIGH);
    digitalWrite(STATUS_LED, HIGH);
    Serial.print("ALARM: LEAK DETECTED! Level: ");
    Serial.println(currentLevel);
  } 
  else if (currentLevel < DRY_THRESHOLD && alarmActive) {
    alarmActive = false;
    digitalWrite(BUZZER_PIN, LOW);
    digitalWrite(STATUS_LED, LOW);
    Serial.println("CLEAR: Sensor is dry.");
  }

  // Throttled Serial Output for plotting
  if (millis() - lastPrintTime >= 500) {
    lastPrintTime = millis();
    Serial.print("Raw: ");
    Serial.print(rawValue);
    Serial.print(" | Smooth: ");
    Serial.println(currentLevel);
  }
  
  delay(50); // Allow ADC to settle
}

void triggerErrorBlink() {
  for(int i=0; i<5; i++) {
    digitalWrite(STATUS_LED, HIGH);
    delay(100);
    digitalWrite(STATUS_LED, LOW);
    delay(100);
  }
}

Debugging: First Three Things to Check When It Fails

When your serial monitor outputs garbage or the alarm triggers on a dry breadboard, do not rewrite the code. Hardware physics is almost always the culprit. Here are the first three things to check, ranked by frequency.

1. The Analog Pin is Floating (Random 300-800 values when dry)

Symptom: The serial monitor outputs erratic values like Raw: 412 | Smooth: 398 even when the sensor is sitting on a dry desk. Touching the wire with your finger makes the numbers jump wildly.

Cause: You forgot the 10kΩ pull-down resistor, or it is not making contact in the breadboard. The ATmega328P ADC has an input impedance of roughly 100MΩ. Without a path to ground, the pin acts as an antenna, picking up 50/60Hz mains hum and RF interference.

Fix: Measure the resistance between the A0 pin and the GND pin with a multimeter while the board is unpowered. You should read exactly 10kΩ (±5%). If you read >1MΩ, reseat the resistor.

2. Sensor Reads 'Wet' Permanently After Two Weeks

Symptom: The system works perfectly on day one. By day 14, the serial monitor reads Raw: 850 constantly, and the buzzer won't stop, even when you wipe the sensor dry.

Cause: Galvanic corrosion (electrolysis). When you apply a constant 5V DC potential across the sensor tines in the presence of even slight humidity, the copper on the anode oxidizes and dissolves into the water, leaving behind a conductive crust of copper salts that permanently bridges the gap.

Fix: You cannot reverse this damage; the sensor must be replaced. To prevent it in future builds, do not power the sensor continuously. Wire the sensor VCC to a digital pin (e.g., D7) and set it HIGH for only 50 milliseconds before taking an analogRead(), then set it LOW. This pulsed-DC approach virtually eliminates electrolysis.

3. Arduino Resets When Water is Detected

Symptom: You dip the sensor in water. The buzzer chirps once, the onboard LED flashes, and then the Arduino completely reboots, restarting the setup sequence.

Cause: Voltage sag on the 5V rail. Cheap 5V active piezo buzzers can draw 30mA to 50mA of inrush current when they activate. If you are powering the Arduino via a weak USB port or a depleted 9V battery, the voltage regulator drops out, causing a brownout reset.

Fix: Solder a 100µF electrolytic capacitor across the buzzer's VCC and GND pins to supply the inrush current locally. Alternatively, power the buzzer from a separate 5V supply, ensuring the grounds are tied together.

Extending and Simplifying the Build

Depending on your application, you may want to strip this project down to its bare essentials or scale it up into a smart-home IoT node.

How to Simplify: Use the Digital Output

If you do not care about how much water is present, only if water is present, abandon the analog pin entirely. The FC-37 module includes an LM393 comparator. Use a small Phillips screwdriver to turn the blue trimpot on the module while the sensor is in a puddle. Adjust it until the digital output (D0) flips from HIGH to LOW. Wire D0 to Arduino Pin 2 and use a simple digitalRead() with an internal pull-up. This eliminates the need for the pull-down resistor, the EMA smoothing code, and ADC noise debugging.

How to Extend: Go Capacitive with an ESP32

Resistive sensors will always eventually fail due to corrosion. To build a maintenance-free system, upgrade to an ESP32 DevKit v1 and use its built-in capacitive touch pins. By taping a copper coin to a touchRead() pin and routing the wire to the lowest point of your water heater pan, you can detect water through the plastic tape or a thin non-conductive barrier. For implementation details, review the Espressif Touch Pad API documentation. From there, you can add an MQTT payload to push alerts directly to Home Assistant.

Feature Resistive (FC-37 / Uno) Digital Comparator (LM393 / Uno) Capacitive Touch (ESP32)
Complexity Medium (Requires ADC filtering) Low (Simple logic HIGH/LOW) High (Requires calibration)
Corrosion Risk High High None (Can be insulated)
Data Granularity Volume estimation (0-1023) Binary (Wet/Dry) Proximity / Binary
Best Use Case Temporary leak testing Sump pump overflow alarm Permanent under-sink IoT monitor

By understanding the physical limitations of resistive traces and the electrical realities of high-impedance ADC pins, you can move past the 'copy-paste' tutorial phase and build a water sensor Arduino system that actually survives the environment it is meant to monitor.