Water Tank Monitor with ESP32 and OpenWRT
projects

Water Tank Monitor with ESP32 and OpenWRT

An ultrasonic distance sensor on an ESP32-C3 posts water level readings over WiFi to a minimal Rust server running on an OpenWRT router — no cloud, no app, just an email when the tank runs low.

If you're a developer who's been curious about hardware but assumed it was too complex or too expensive, this project shows how far you can get in an afternoon with an ESP32, a Qwiic sensor, and no soldering.

Why

I live somewhere where water supply interruptions happen without warning. A horse trampled a section of overland pipe once. Road workers cut a line another time. Routine maintenance, no notification. The first sign is always standing in the shower with no water.

I'd been curious about hardware projects for years. I knew Arduino existed, but the price and size always felt like overkill for something small. Then I got into hydroponics and discovered the ESP32 — and realized the barrier was much lower than I thought.

The prototype took less than two hours. I already had the hardware.

What

The system has two parts.

The sensor side: a SparkFun Pro Micro ESP32-C3 (RISC-V, WiFi, Qwiic connector) with a SparkFun Qwiic Ultrasonic sensor for the prototype, replaced by a waterproof JSN-SR04T in production. The ESP32 measures the distance from the sensor to the water surface, converts it to a percentage, and POSTs the reading over WiFi. Then it disconnects WiFi and goes into deep sleep for 10 minutes.

The router side: a GL.iNet GL-MT3000 running OpenWRT — already on, already drawing power. A small Rust server receives the readings, stores the latest state in memory, serves a dashboard, and sends an email alert (max once per hour) when the level drops below the threshold.

No cloud. No app. Email is enough — apps come and go, email doesn't.

How

The ESP32 firmware is C++ with the Arduino framework via PlatformIO. The Qwiic sensor speaks I2C, which meant no soldering — just plug in the cable. The only real hurdle was finding the right pins: the SparkFun Pro Micro ESP32-C3 uses GPIO 5/6 for Qwiic, not the ESP32-C3 defaults of 8/9. A web search for the board datasheet solved it in five minutes.

#define SDA_PIN 5
#define SCL_PIN 6

PlatformIO has three environments. dev posts continuously to a local machine for debugging. stage posts to the router continuously. prod enables deep sleep — one reading every 10 minutes, WiFi off in between.

[env:prod]
build_flags =
-D USE_DEEP_SLEEP
-D SLEEP_INTERVAL_MIN=10

The deep sleep flag is the difference between a battery-viable sensor and one that drains in a day. The same codebase handles both:

void setup() {
Wire.begin(SDA_PIN, SCL_PIN);
sensor.begin();
#ifdef USE_DEEP_SLEEP
float dist = measure_distance_cm();
post_reading(to_level_pct(dist), dist);
WiFi.disconnect(true);
esp_deep_sleep((uint64_t)SLEEP_INTERVAL_MIN * 60ULL * 1000000ULL);
#endif
}

The Rust server runs on the router. OpenWRT is just Linux — cross-compiling to aarch64-unknown-linux-musl and deploying a single binary is all it takes. Rust's static linking means no dependencies on the router side. An init.d entry keeps it running across reboots.

The server itself is Axum: one POST /level endpoint that updates in-memory state and triggers the email alert if needed, one GET /api/status for the current reading, and a dashboard served as a static HTML file embedded directly in the binary with include_str!. No filesystem access at runtime.

async fn get_dashboard() -> Html<&'static str> {
Html(include_str!("dashboard.html"))
}

The alert has a one-hour cooldown so a low tank doesn't generate 144 emails per day.

The bigger picture

Everything that felt fragile about my water situation is now visible. I get an email before the tank is empty, not after. And the whole thing runs on hardware that was already powered on.

What surprised me more than the result was how little friction there was. Most sensors available today are plug-and-play — I2C, UART, or SPI, often with Qwiic or similar no-solder connectors. The protocols sound intimidating but you don't need to understand the wire-level details to use them. You need to know: which protocol does this sensor speak, and which pins on my board.

The ESP32 is the right starting point for software developers. It's cheap (a few dollars/euros), small, has WiFi built in, and the Arduino framework means you're writing C++ with a standard library that handles the hardware abstractions. PlatformIO makes the build setup feel familiar — environment configs, build flags, deploy targets.

The router as a server is something I'd use again. It's always on, it's on the local network, and cross-compiling a Rust binary for it is straightforward. No cloud account, no monthly cost, no dependency on someone else's uptime.