An Arduino water detector relies on the electrical conductivity of water to close a circuit between exposed traces. While chemically pure H2O is an insulator, everyday tap water contains dissolved ions (calcium, magnesium, sodium) that drop the electrical resistance between sensor prongs from over 10 MΩ when dry to under 50 kΩ when submerged. By measuring this resistance change via a voltage divider and an analog-to-digital converter (ADC), a microcontroller can reliably trigger local alarms or push WiFi alerts to your phone before a minor leak becomes a major flood.

This guide walks through building a networked water leak detector targeting the NodeMCU ESP8266 V3 (CP2102 variant). We will cover empirical ADC calibration data, exact wiring, production-ready C++ firmware with HTTP webhook alerts, and how to debug the most common radio and sensor failures.

Hardware Specifications and Sensor Calibration Data

Before writing code, you need to understand what the microcontroller actually sees. The FC-37 water sensor is essentially a bare printed circuit board with interlocking copper traces. When wired in series with a pull-down resistor, it forms a voltage divider. The table below maps real-world water conditions to the expected 10-bit ADC values on a 3.3V logic system (like the ESP8266) using a 10kΩ pull-down resistor to ground.

Table 1: FC-37 Sensor Resistance vs. ADC Reading (3.3V System, 10kΩ Pull-Down)
Condition Water Depth / Type Approx. Resistance Voltage at A0 ESP8266 ADC (10-bit)
Dry (Air) 0 mm > 10 MΩ 0.00V 0 - 5
Light Mist Condensation ~ 500 kΩ 0.06V 15 - 40
Partial Submersion 5 mm (Tap Water) ~ 45 kΩ 0.60V 180 - 250
Full Submersion 20 mm (Tap Water) ~ 8 kΩ 1.85V 550 - 650
Full Submersion 20 mm (Salt Water) ~ 1.2 kΩ 2.87V 880 - 950

Note: According to the USGS Water Science School, dissolved solids drastically alter conductivity. Always calibrate your specific threshold using the actual water source (e.g., condensation from an AC unit vs. a leaking washing machine) you intend to monitor.

Parts List (2026 Bench-Tested Variants)

  • Microcontroller: NodeMCU ESP8266 V3 (Specifically the CP2102 USB-UART variant, ~$6.50. Avoid the CH340 variants if you frequently use macOS or Linux, as driver signing can be problematic).
  • Sensor: FC-37 Raindrop/Water Sensor Module with LM393 Comparator (~$2.50). We will use the analog (A0) output, not the digital (D0) output.
  • Resistor: 10kΩ 1/4W Metal Film Resistor (for the A0 pull-down, ~$0.10).
  • Alarm: TMB12A03 Active Piezo Buzzer (3.3V/5V compatible, ~$1.20).
  • Power: 5V 2A USB power supply (The ESP8266 draws up to 170mA during WiFi TX bursts; a weak supply will cause brownouts).

Pin Mapping and Wiring Steps

The ESP8266 has only one usable analog input pin (A0), which is perfect for a single-point water detector. The internal ADC measures 0-1.0V, but the NodeMCU V3 board includes an onboard voltage divider (220kΩ/100kΩ) that scales the 0-3.3V input down to the chip's safe range. Do not feed 5V into the A0 pin, or you will fry the ESP8266 silicon.

Table 2: NodeMCU ESP8266 to FC-37 Pin Mapping
Component Component Pin NodeMCU Pin Wire Color (Typical)
FC-37 Sensor VCC 3V3 Red
FC-37 Sensor GND GND Black
FC-37 Sensor A0 (Analog Out) A0 Yellow
10kΩ Resistor Leg 1 A0 (shared) N/A
10kΩ Resistor Leg 2 GND (shared) N/A
Piezo Buzzer + (Signal) D5 (GPIO14) Orange
Piezo Buzzer - (Ground) GND Brown

Wiring Procedure

  1. Mount the pull-down resistor: Insert the 10kΩ resistor into the breadboard. Connect one leg to the NodeMCU's GND rail and the other leg to the A0 pin row. Why? Bare copper traces act as antennas when dry. Without this resistor, the A0 pin will float, picking up 60Hz mains hum and triggering false alarms.
  2. Wire the sensor: Connect the FC-37 VCC to the NodeMCU 3V3 pin. Connect FC-37 GND to the breadboard GND rail. Connect the FC-37 A0 pin to the NodeMCU A0 pin (sharing the row with the 10kΩ resistor).
  3. Wire the buzzer: Connect the active buzzer's positive lead to D5 (GPIO14) and the negative lead to GND. Ensure you are using an active buzzer (which has an internal oscillator) rather than a passive one, so the microcontroller only needs to output a steady HIGH signal to generate sound.
  4. Position the sensor: Mount the FC-37 board vertically in a 3D-printed housing or hot-glue it to a Popsicle stick so the traces face the floor. The bottom edge of the traces should sit exactly 2mm above the floor surface to catch spreading water before it reaches critical appliances.

Complete Firmware with Error Handling

This firmware targets the NodeMCU ESP8266 V3 (CP2102) using the Arduino IDE (Board Manager: esp8266 by ESP8266 Community, version 3.1.2 or newer). It reads the analog pin, applies a 10-sample rolling average to filter out surface tension noise, triggers a local buzzer, and sends an HTTP POST to a webhook (like IFTTT or Home Assistant) when water is detected.

#include 
#include 
#include 

// --- PIN DEFINITIONS ---
#define SENSOR_PIN A0
#define BUZZER_PIN D5 // GPIO14

// --- CALIBRATION THRESHOLDS ---
#define DRY_THRESHOLD 50      // ADC value below which we consider it 'dry'
#define WET_THRESHOLD 250     // ADC value that triggers a leak alarm
#define SAMPLE_SIZE 10        // Number of reads for rolling average

// --- NETWORK CREDENTIALS ---
const char* ssid = "Your_2.4GHz_Network";
const char* password = "Your_Password";
const char* webhook_url = "http://192.168.1.100:8123/api/webhook/leak_detected";

// --- STATE VARIABLES ---
unsigned long lastAlertTime = 0;
const unsigned long alertCooldown = 300000; // 5 minutes between HTTP alerts
bool isLeaking = false;

void setup() {
  Serial.begin(115200);
  delay(100);
  Serial.println("\n[BOOT] Arduino Water Detector Initializing...");
  
  pinMode(BUZZER_PIN, OUTPUT);
  digitalWrite(BUZZER_PIN, LOW);
  
  // Brief startup beep to confirm buzzer wiring
  digitalWrite(BUZZER_PIN, HIGH);
  delay(200);
  digitalWrite(BUZZER_PIN, LOW);
  
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  Serial.print("[WIFI] Connecting to ");
  Serial.print(ssid);
  
  int attempts = 0;
  while (WiFi.status() != WL_CONNECTED && attempts < 20) {
    delay(500);
    Serial.print(".");
    attempts++;
  }
  
  if (WiFi.status() == WL_CONNECTED) {
    Serial.println("\n[WIFI] Connected! IP: " + WiFi.localIP().toString());
  } else {
    Serial.println("\n[ERROR] WiFi connection failed. Status: " + String(WiFi.status()));
    // Blink buzzer pattern to indicate network failure (3 short beeps)
    for(int i=0; i<3; i++) {
      digitalWrite(BUZZER_PIN, HIGH); delay(100);
      digitalWrite(BUZZER_PIN, LOW); delay(100);
    }
  }
}

void loop() {
  // 1. Read sensor with rolling average to debounce water ripples
  long total = 0;
  for (int i = 0; i < SAMPLE_SIZE; i++) {
    total += analogRead(SENSOR_PIN);
    delay(5); // Allow ADC capacitor to settle
  }
  int avgReading = total / SAMPLE_SIZE;
  
  // 2. Evaluate state
  if (avgReading >= WET_THRESHOLD && !isLeaking) {
    isLeaking = true;
    digitalWrite(BUZZER_PIN, HIGH);
    Serial.println("[ALERT] LEAK DETECTED! ADC: " + String(avgReading));
    sendWebhookAlert(avgReading);
  } 
  else if (avgReading < DRY_THRESHOLD && isLeaking) {
    isLeaking = false;
    digitalWrite(BUZZER_PIN, LOW);
    Serial.println("[STATUS] Sensor dry. System reset. ADC: " + String(avgReading));
  }
  
  // 3. Serial logging every 2 seconds
  static unsigned long lastLog = 0;
  if (millis() - lastLog > 2000) {
    Serial.println("[DEBUG] Current ADC: " + String(avgReading) + " | State: " + (isLeaking ? "WET" : "DRY"));
    lastLog = millis();
  }
  
  delay(100);
}

void sendWebhookAlert(int adcValue) {
  if (WiFi.status() != WL_CONNECTED) {
    Serial.println("[HTTP] Skipped: WiFi disconnected.");
    return;
  }
  
  // Enforce cooldown to prevent spamming the server
  if (millis() - lastAlertTime < alertCooldown) {
    Serial.println("[HTTP] Skipped: Cooldown active.");
    return;
  }
  
  WiFiClient client;
  HTTPClient http;
  http.begin(client, webhook_url);
  http.addHeader("Content-Type", "application/json");
  
  String payload = "{\"sensor\": \"fc37_kitchen\", \"adc_raw\": " + String(adcValue) + "}";
  int httpCode = http.POST(payload);
  
  if (httpCode > 0) {
    Serial.println("[HTTP] Alert sent. Response code: " + String(httpCode));
    lastAlertTime = millis();
  } else {
    Serial.println("[HTTP] Request failed. Error: " + http.errorToString(httpCode));
  }
  http.end();
}

Debugging: 'NO_SSID_AVAIL' and Sensor Drift

When deploying embedded sensors, hardware and RF environments rarely behave exactly as they do on the bench. If your serial monitor outputs the exact error string [ERROR] WiFi connection failed. Status: 1 (which maps to the WL_NO_SSID_AVAIL enumeration in the ESP8266WiFi library), or if your sensor readings drift over time, follow this diagnostic path.

The First Three Things to Check for WiFi Failures

  1. 2.4GHz vs 5GHz Band Mismatch: The ESP8266 silicon only supports 802.11b/g/n on the 2.4GHz band. If your router uses a unified SSID for both 2.4GHz and 5GHz, the ESP8266 radio stack can sometimes fail to negotiate the handshake. Fix: Create a dedicated 2.4GHz-only IoT SSID on your router, or force the router to use WPA2-PSK (AES) instead of WPA3, which the ESP8266 struggles with natively.
  2. Hidden SSIDs: The ESP8266 AT firmware and standard Arduino core do not reliably probe for hidden networks. Fix: Unhide the SSID in your router settings. Security through obscurity provides negligible protection against modern wardriving anyway.
  3. 3.3V Brownout During TX Bursts: When the ESP8266 transmits a WiFi packet, it draws a transient spike of up to 170mA. If your USB cable has high resistance or the NodeMCU's onboard AMS1117-3.3 LDO regulator is overheating, the voltage dips below 2.8V, resetting the radio. Fix: Use a high-quality, short USB cable rated for data and 2A charging, or solder a 100µF electrolytic capacitor directly across the 3V3 and GND pins on the breadboard to buffer the transient load.

Sensor Drift and Electrolysis (The Silent Killer)

If your water detector works perfectly on day one but triggers false alarms on day 14, you are experiencing electrolysis. Because the FC-37 is constantly powered with a DC voltage (3.3V) across the traces, the dissolved ions in tap water cause a galvanic reaction. The copper traces will literally dissolve into the water, leaving behind a green/blue crust (copper chloride) that becomes conductive even when the water dries up.

The Fix: Do not leave the sensor powered continuously. Modify the code to power the sensor from a GPIO pin instead of the 3V3 rail. Set the GPIO HIGH for 50 milliseconds, take the analogRead(), and immediately set the GPIO LOW. This limits the DC current flow to less than 1% of the time, extending the sensor's lifespan from weeks to years.

Extending and Simplifying the Build

Depending on your deployment environment, you may need to scale this project up for whole-home automation or strip it down for a standalone, offline appliance monitor.

How to Extend: Add MQTT and Automatic Shut-Off

For advanced home automation, replace the HTTP webhook with the MQTT protocol using the PubSubClient library. MQTT maintains a persistent, low-overhead TCP connection, making it ideal for battery-backed leak sensors.

To make the system actively stop a leak, add a 12V DC solenoid water valve to your main water line. Because the ESP8266 GPIO pins can only source ~12mA, you cannot drive a solenoid directly. Use a logic-level N-channel MOSFET like the IRLZ44N. Connect the ESP8266 D6 pin to the MOSFET gate via a 220Ω resistor, the solenoid to the 12V supply and the MOSFET drain, and the MOSFET source to ground. Add a 1N4007 flyback diode across the solenoid coils to protect the MOSFET from inductive voltage spikes when the valve closes.

How to Simplify: Zero-Code Analog Comparator

If you don't need WiFi alerts and just want a local alarm for a basement sump pump, you can delete the microcontroller entirely. The FC-37 module kit usually includes an LM393 comparator board.

Wire the bare FC-37 sensor board to the comparator module's input. Connect the module's VCC to a 9V battery or 5V USB pack. Turn the blue potentiometer on the LM393 board with a small flathead screwdriver until the onboard LED turns off. When water bridges the sensor traces, the voltage at the input pin rises above the potentiometer's reference voltage, the LM393 output pulls LOW, and triggers a connected 5V active buzzer. This purely analog approach costs under $4, requires zero C++ code, and is immune to WiFi outages.