Every Indian home with an overhead tank has had it overflow or run dry. The JSN-SR04T is a waterproof ultrasonic probe made for this job, and the Pico 2 W reads it and sends alerts over Wi-Fi.
Power the sensor from VBUS, send Trig from GP14, and bring Echo down to 3.3 V with a 1 kΩ / 2 kΩ divider into GP15. Enter your tank’s depth, and the script prints the level every 10 seconds and alerts your phone when the tank is low or full.
Wiring
| Raspberry Pi Pico 2 W | JSN-SR04T waterproof ultrasonic sensor | Wire | Notes |
|---|---|---|---|
| VBUS | 5V | red | VBUS (pin 40), USB 5 V — the most reliable supply for this sensor. |
| GP14 | Trig | purple | GP14 is physical pin 19. A 3.3 V pulse is enough. |
| GP15 | Echo | green | Via 1 kΩ / 2 kΩ divider — Echo is a 5 V pulse. 1 kΩ from Echo to GP15, 2 kΩ from GP15 to GND. GP15 is physical pin 20. |
| GND | GND | black |
5 V (V3.0 boards: 3.0–5.5 V) part, 3.3 V board. Echo swings to the supply voltage. Powered from 5 V (most reliable), send Echo to an ESP32 or Pico through a 1 kΩ / 2 kΩ divider. A 3.3 V Trig pulse is enough.
Board quirks
- The Pico 2 W’s GPIOs tolerate 5 V only while the board is powered (and the ADC pins never do), so this guide keeps the divider — a power cut that leaves the sensor powered first can’t hurt the board.
- The probe has a blind zone of about 20–25 cm. Mount it at least 25 cm above the overflow level and set
FULL_GAP_CMto that distance,TANK_DEPTH_CMto sensor-to-floor. - A 20 µs trigger pulse and the median of seven pings ride out the echoes from tank walls and the inlet pipe.
- Optional ntfy alerts at 20 % (“tank low”) and 95 % (“switch the motor off”). Switching a 230 V pump automatically needs a contactor and an electrician — this sketch only alerts.
Code
# Water-tank level with a JSN-SR04T waterproof ultrasonic sensor on a Raspberry Pi Pico 2 W
# (MicroPython), with optional "tank low" / "tank full" phone alerts through ntfy.sh.
# Wiring: 5V -> VBUS (pin 40), GND -> GND, Trig -> GP14 (pin 19),
# Echo -> 1 kOhm -> GP15 (pin 20), and 2 kOhm from GP15 to GND (Echo is a 5 V pulse).
from machine import Pin, time_pulse_us
import network
import time
import json
TANK_DEPTH_CM = 120 # sensor face to the tank floor <- measure yours
FULL_GAP_CM = 25 # sensor face to the water when full (>= 25 cm: the sensor's blind zone)
LOW_PCT = 20
FULL_PCT = 95
WIFI_SSID = "your-wifi" # optional
WIFI_PASS = "your-password"
NTFY_TOPIC = "your-private-topic" # https://ntfy.sh/<topic> - pick something unguessable
# The FlashReady flasher saves your settings to /settings.json; they win over the lines above.
try:
with open("/settings.json") as f:
_s = json.load(f)
except (OSError, ValueError):
_s = {}
WIFI_SSID = _s.get("wifi_ssid") or WIFI_SSID
WIFI_PASS = _s.get("wifi_pass") or WIFI_PASS
NTFY_TOPIC = _s.get("ntfy_topic") or NTFY_TOPIC
ALERTS = WIFI_SSID != "your-wifi" and NTFY_TOPIC != "your-private-topic"
if not ALERTS:
print("Phone alerts off - no Wi-Fi name or ntfy topic set. Flash from flashready.in or type them into main.py.")
trig = Pin(14, Pin.OUT, value=0)
echo = Pin(15, Pin.IN)
led = Pin("LED", Pin.OUT)
def ping_cm():
trig.value(1)
time.sleep_us(20) # the JSN-SR04T wants a longer trigger than the HC-SR04
trig.value(0)
t = time_pulse_us(echo, 1, 40000)
return None if t < 0 else t * 0.0343 / 2
def median_cm(n=7):
vals = []
for _ in range(n):
d = ping_cm()
if d is not None and d > 20: # inside the blind zone = not trustworthy
vals.append(d)
time.sleep_ms(100)
vals.sort()
return vals[len(vals) // 2] if vals else None
def notify(msg):
if not ALERTS:
return
wlan = network.WLAN(network.STA_IF)
wlan.active(True)
if not wlan.isconnected():
wlan.connect(WIFI_SSID, WIFI_PASS)
for _ in range(30):
if wlan.isconnected():
break
time.sleep(0.5)
if not wlan.isconnected():
print("Wi-Fi failed - no alert sent")
return
try:
import requests
except ImportError:
import urequests as requests
try:
requests.post("https://ntfy.sh/" + NTFY_TOPIC, data=msg).close()
print("Phone alert sent")
except OSError as e:
print("ntfy failed:", e)
print("Tank depth {} cm, full at {} cm from the sensor".format(TANK_DEPTH_CM, FULL_GAP_CM))
state = None
while True:
d = median_cm()
if d is None:
print("No echo - check the sensor and divider")
else:
water = TANK_DEPTH_CM - d
pct = max(0, min(100, int(water * 100 / (TANK_DEPTH_CM - FULL_GAP_CM))))
print("Distance {:.1f} cm -> water {:.0f} cm ({} %)".format(d, water, pct))
led.value(pct <= LOW_PCT or pct >= FULL_PCT)
if pct <= LOW_PCT and state != "low":
state = "low"
notify("Water tank low: {} %".format(pct))
elif pct >= FULL_PCT and state != "full":
state = "full"
notify("Water tank full: {} % - switch the motor off".format(pct))
elif LOW_PCT + 10 < pct < FULL_PCT - 10:
state = "mid" # re-arm both alerts
time.sleep(10)# Water-tank level with a JSN-SR04T waterproof ultrasonic sensor on a Raspberry Pi Pico 2 W
# (MicroPython), with optional "tank low" / "tank full" phone alerts through ntfy.sh.
# Wiring: 5V -> VBUS (pin 40), GND -> GND, Trig -> GP14 (pin 19),
# Echo -> 1 kOhm -> GP15 (pin 20), and 2 kOhm from GP15 to GND (Echo is a 5 V pulse).
from machine import Pin, time_pulse_us
import network
import time
import json
TANK_DEPTH_CM = 120 # sensor face to the tank floor <- measure yours
FULL_GAP_CM = 25 # sensor face to the water when full (>= 25 cm: the sensor's blind zone)
LOW_PCT = 20
FULL_PCT = 95
WIFI_SSID = "your-wifi" # optional
WIFI_PASS = "your-password"
NTFY_TOPIC = "your-private-topic" # https://ntfy.sh/<topic> - pick something unguessable
# The FlashReady flasher saves your settings to /settings.json; they win over the lines above.
try:
with open("/settings.json") as f:
_s = json.load(f)
except (OSError, ValueError):
_s = {}
WIFI_SSID = _s.get("wifi_ssid") or WIFI_SSID
WIFI_PASS = _s.get("wifi_pass") or WIFI_PASS
NTFY_TOPIC = _s.get("ntfy_topic") or NTFY_TOPIC
ALERTS = WIFI_SSID != "your-wifi" and NTFY_TOPIC != "your-private-topic"
if not ALERTS:
print("Phone alerts off - no Wi-Fi name or ntfy topic set. Flash from flashready.in or type them into main.py.")
trig = Pin(14, Pin.OUT, value=0)
echo = Pin(15, Pin.IN)
led = Pin("LED", Pin.OUT)
def ping_cm():
trig.value(1)
time.sleep_us(20) # the JSN-SR04T wants a longer trigger than the HC-SR04
trig.value(0)
t = time_pulse_us(echo, 1, 40000)
return None if t < 0 else t * 0.0343 / 2
def median_cm(n=7):
vals = []
for _ in range(n):
d = ping_cm()
if d is not None and d > 20: # inside the blind zone = not trustworthy
vals.append(d)
time.sleep_ms(100)
vals.sort()
return vals[len(vals) // 2] if vals else None
def notify(msg):
if not ALERTS:
return
wlan = network.WLAN(network.STA_IF)
wlan.active(True)
if not wlan.isconnected():
wlan.connect(WIFI_SSID, WIFI_PASS)
for _ in range(30):
if wlan.isconnected():
break
time.sleep(0.5)
if not wlan.isconnected():
print("Wi-Fi failed - no alert sent")
return
try:
import requests
except ImportError:
import urequests as requests
try:
requests.post("https://ntfy.sh/" + NTFY_TOPIC, data=msg).close()
print("Phone alert sent")
except OSError as e:
print("ntfy failed:", e)
print("Tank depth {} cm, full at {} cm from the sensor".format(TANK_DEPTH_CM, FULL_GAP_CM))
state = None
while True:
d = median_cm()
if d is None:
print("No echo - check the sensor and divider")
else:
water = TANK_DEPTH_CM - d
pct = max(0, min(100, int(water * 100 / (TANK_DEPTH_CM - FULL_GAP_CM))))
print("Distance {:.1f} cm -> water {:.0f} cm ({} %)".format(d, water, pct))
led.value(pct <= LOW_PCT or pct >= FULL_PCT)
if pct <= LOW_PCT and state != "low":
state = "low"
notify("Water tank low: {} %".format(pct))
elif pct >= FULL_PCT and state != "full":
state = "full"
notify("Water tank full: {} % - switch the motor off".format(pct))
elif LOW_PCT + 10 < pct < FULL_PCT - 10:
state = "mid" # re-arm both alerts
time.sleep(10)Expected serial output
Tank depth 120 cm, full at 25 cm from the sensor
Distance 74.2 cm -> water 46 cm (48 %)
Distance 29.8 cm -> water 90 cm (95 %)
Phone alert sentTank depth 120 cm, full at 25 cm from the sensor Distance 74.2 cm -> water 46 cm (48 %) Distance 29.8 cm -> water 90 cm (95 %) Phone alert sent
Common errors
Reads about 20 cm when the tank is nearly full
The water is inside the blind zone. Raise the sensor so the full level is at least 25 cm below it.
“No echo” every time
Check the divider orientation (1 kΩ next to the sensor), the 20 µs trigger, and that the R27 mode resistor on V3.0 boards is open (Trig/Echo mode).
Level jumps in a narrow tank
The wide beam hits the walls. Mount the probe in the centre, away from the inlet pipe.