Difficulty: 2/5 (Basic wiring, intermediate C++ logic)
Time to Bench-Test: 45 minutes
Estimated Cost: $12 - $18 USD
Target Board: Arduino Uno R3 (ATmega328P) or Nano v3
If you are building an Arduino water level sensor, you have two dominant hardware paths: the cheap, exposed-trace FC-28 resistive module, or the sealed, non-contact JSN-SR04T ultrasonic transducer. The direct answer to "which one should I use?" is the JSN-SR04T for any long-term or dirty-water application. The FC-28 will suffer from galvanic corrosion and dissolve its own copper traces within 72 hours if left continuously submerged and powered. Use the FC-28 only for temporary, clean-water prototyping or as a simple high/low digital switch.
Below is the complete bench-to-deployment guide for wiring, coding, and debugging both modules, with a heavy emphasis on the ultrasonic path since it is the only viable choice for permanent installations.
The Decision Path: FC-28 vs JSN-SR04T
Before cutting wires, run your project requirements through this decision matrix. Do not default to the FC-28 just because it costs $1.50; the replacement labor will cost you far more.
| Condition / Requirement | FC-28 Resistive Module | JSN-SR04T Ultrasonic |
|---|---|---|
| Water Quality (Dirty/Mineral-heavy) | Fails rapidly (shorts via mineral buildup) | Unaffected (non-contact) |
| Deployment Duration (> 1 week) | Corrodes via electrolysis | Years (IP67 sealed transducer) |
| Measurement Type Needed | Threshold (High/Low) only | Continuous distance/volume |
| Mounting Constraint | Must touch liquid | Must mount above liquid (min 20cm blind zone) |
The Verdict: For 95% of DIY sump pumps, rain barrels, and hydroponic reservoirs, the JSN-SR04T v2.0 is the mandatory pick. The FC-28 is strictly reserved for classroom demos or temporary leak-detection mats where power is only applied during the read cycle.
Parts List and Specifications
Here is the exact bill of materials (BOM) for a robust ultrasonic build. Pricing reflects typical 2026 hobbyist supplier rates.
| Component | Exact Variant / Model | Key Spec / Note | Est. Price |
|---|---|---|---|
| Microcontroller | Arduino Uno R3 (or Nano v3) | ATmega328P, 5V logic | $14.00 |
| Ultrasonic Sensor | JSN-SR04T v2.0 | 2.5V to 5.5V, 20cm-450cm range, IP67 probe | $4.50 |
| Resistive Sensor (Alt) | FC-28 with LM393 comparator | Analog & Digital out, 3.3V-5V | $1.50 |
| Relay Module | 5V 1-Channel Opto-isolated | Active LOW, flyback diode included | $2.00 |
| Pull-down Resistor | 10kΩ (1/4W) | Prevents floating Echo pin noise | $0.10 |
Wiring and Pin Mapping
Ultrasonic sensors are notoriously susceptible to noise on the Echo line. If you run the Echo wire parallel to a pump motor's power cable, you will get phantom readings. Keep signal wires separated from inductive loads.
| Module Pin | Arduino Uno R3 Pin | Wire Color (Standard) | Notes & Hardware Gotchas |
|---|---|---|---|
| JSN-SR04T VCC | 5V | Red | Do NOT use 3.3V; the transducer drive requires 5V. |
| JSN-SR04T Trig | D9 | Yellow | Output pin. 10µs HIGH pulse triggers the burst. |
| JSN-SR04T Echo | D10 | Blue | Input pin. Wire a 10kΩ resistor from D10 to GND to prevent floating state timeouts. |
| JSN-SR04T GND | GND | Black | Must share common ground with the Arduino. |
| Relay IN | D8 | Green | Active LOW. Pin HIGH = relay OFF. |
Complete Arduino Code with Error Handling
This code targets the Arduino Uno R3 (ATmega328P). It avoids third-party libraries like NewPing to ensure it compiles cleanly out-of-the-box on any IDE version, using raw pulseIn() with strict timeout and bounds-checking error handling. It also implements a moving average filter to eliminate "multipath jitter"—a common issue when ultrasonic waves bounce off the walls of narrow PVC standpipes.
// Arduino Water Level Sensor - JSN-SR04T Robust Implementation
// Target: Arduino Uno R3 / Nano v3 (5V Logic)
#define TRIG_PIN 9
#define ECHO_PIN 10
#define RELAY_PIN 8
// Tank Configuration
#define TANK_HEIGHT_CM 100.0 // Distance from sensor to tank floor
#define EMPTY_THRESHOLD_CM 85.0 // Turn pump ON when distance > 85cm
#define FULL_THRESHOLD_CM 20.0 // Turn pump OFF when distance < 20cm
#define MAX_VALID_DISTANCE 450 // JSN-SR04T max reliable range
// Filter Configuration
const int numReadings = 10;
int readings[numReadings];
int readIndex = 0;
long total = 0;
void setup() {
Serial.begin(115200);
pinMode(TRIG_PIN, OUTPUT);
pinMode(ECHO_PIN, INPUT);
pinMode(RELAY_PIN, OUTPUT);
digitalWrite(RELAY_PIN, HIGH); // Active LOW relay: HIGH = OFF
// Initialize filter array
for (int i = 0; i < numReadings; i++) {
readings[i] = 0;
}
Serial.println("System Initialized. Monitoring water level...");
}
void loop() {
long duration, distance;
// 1. Trigger the ultrasonic burst
digitalWrite(TRIG_PIN, LOW);
delayMicroseconds(2);
digitalWrite(TRIG_PIN, HIGH);
delayMicroseconds(10);
digitalWrite(TRIG_PIN, LOW);
// 2. Read the echo with a strict timeout (Max 450cm = ~26ms)
// pulseIn returns 0 if timeout is reached
duration = pulseIn(ECHO_PIN, HIGH, 30000UL);
// 3. Calculate distance (Speed of sound ~343 m/s at 20C -> 0.0343 cm/uS)
distance = (duration / 2.0) * 0.0343;
// 4. Error Handling & Bounds Checking
if (duration == 0 || distance > MAX_VALID_DISTANCE || distance < 2) {
// Sensor timed out, read an impossible physical distance, or hit the blind spot
Serial.println("Error: Invalid sensor read. Check wiring or standpipe alignment.");
delay(500);
return; // Skip this loop iteration, do not update the filter
}
// 5. Moving Average Filter (Eliminates multipath jitter)
total = total - readings[readIndex];
readings[readIndex] = distance;
total = total + readings[readIndex];
readIndex = (readIndex + 1) % numReadings;
long averageDistance = total / numReadings;
float waterLevelCm = TANK_HEIGHT_CM - averageDistance;
if (waterLevelCm < 0) waterLevelCm = 0; // Clamp negative values
Serial.print("Distance: ");
Serial.print(averageDistance);
Serial.print(" cm | Water Level: ");
Serial.print(waterLevelCm);
Serial.println(" cm");
// 6. Pump Control Logic with Hysteresis
if (averageDistance >= EMPTY_THRESHOLD_CM) {
digitalWrite(RELAY_PIN, LOW); // Turn Pump ON
Serial.println("Action: Pump ON (Low Water)");
}
else if (averageDistance <= FULL_THRESHOLD_CM) {
digitalWrite(RELAY_PIN, HIGH); // Turn Pump OFF
Serial.println("Action: Pump OFF (Tank Full)");
}
delay(250); // Read rate limit (prevents echo overlap)
}
Debugging: First Three Things to Check When It Fails
When your serial monitor output doesn't match the physical water level, follow this ranked troubleshooting path. These are the exact failure modes I see most often on the bench.
1. Serial Monitor Prints: Error: Invalid sensor read... or Distance: 0 cm
Ranked Causes:
- Floating Echo Pin: You omitted the 10kΩ pull-down resistor between D10 and GND. The pin is picking up ambient EMI from your bench power supply. Fix: Solder the resistor directly across the Echo and GND pins on the module.
- Voltage Sag on Trig: The JSN-SR04T draws a sharp current spike when firing the transducer. If powered via the Arduino's onboard 5V regulator while also driving a relay, the voltage dips below 4.5V, causing the logic to fail. Fix: Power the sensor and relay directly from a 5V 2A buck converter, sharing only the GND with the Arduino.
- Blind Zone Violation: The water level has risen above the 20cm minimum sensing distance. Fix: Raise the sensor mount.
2. Serial Monitor Prints: Distance: 400 cm (or max timeout value) when the tank is full
Ranked Causes:
- Condensation on Transducer Face: In sealed tanks, high humidity causes water droplets to form on the sensor mesh. This scatters the 40kHz acoustic wave. Fix: Coat the mesh with a thin layer of hydrophobic nano-coating (like Rain-X for plastics) or mount a desiccant pack in the enclosure.
- Foam / Agitation: If the tank is being filled rapidly, surface foam absorbs ultrasonic waves. Fix: Mount the sensor inside a 2-inch PVC standpipe that extends below the water line to read a calm, isolated column of water.
3. Using FC-28: Serial Monitor Prints Reading stuck at 1023 regardless of moisture
Ranked Causes:
- Galvanic Corrosion: The copper traces have literally dissolved due to electrolysis. Inspect the probe; if it looks black or green, it is dead. Fix: Replace probe. To prevent, only power the VCC pin via a digital GPIO (turn it HIGH for 10ms to read, then LOW).
- LM393 Potentiometer Misadjustment: If using the Digital Out (D0) pin, the blue trimpot threshold is set too high. Fix: Submerge probe, use a small Phillips screwdriver to turn the trimpot until the onboard LED toggles.
Extending and Simplifying the Build
Depending on your final deployment environment, you may need to alter the complexity of this circuit.
How to Simplify (No-Code Digital Switch)
If you don't actually need continuous volume data and just want to trigger a pump when water hits a specific line, ditch the Arduino entirely. Use the FC-28 module's LM393 comparator.
Wiring: Connect FC-28 VCC to 5V, GND to GND, and the D0 pin directly to your 5V Relay IN pin. Adjust the blue trimpot to set your trigger depth. The LM393 will pull the D0 pin LOW when the water resistance drops below the threshold, triggering the relay. Total cost: $3.50. Zero code required.
How to Extend (IoT and MQTT Telemetry)
To push water level data to Home Assistant or an MQTT broker, swap the Arduino Uno R3 for an ESP32-WROOM-32 DevKit v1.
Migration Notes:
- The ESP32 operates on 3.3V logic. The JSN-SR04T Echo pin outputs 5V. You MUST use a voltage divider (e.g., 10kΩ and 20kΩ resistors) on the Echo line to step the 5V down to ~3.3V, or you will fry the ESP32 GPIO.
- Use the
PubSubClientlibrary to publish thewaterLevelCmvariable to a topic likehome/tank/levelevery 60 seconds. - Utilize the ESP32's deep sleep capabilities to run the sensor off a 18650 lithium cell for months, waking only to ping the sensor and transmit.
For authoritative details on timing functions used in the code, refer to the official Arduino pulseIn() documentation. For the physics of acoustic velocity used in the distance calculation, the Georgia State University HyperPhysics speed of sound reference provides the exact temperature-compensation math if you decide to add a DS18B20 temperature sensor to your build for sub-millimeter accuracy.
By selecting the JSN-SR04T, implementing a pull-down resistor, and filtering the multipath jitter in software, your Arduino water level sensor will run for years without maintenance.






