An HC-SR04 on the tank lid measures the distance to the water. The UNO R4 WiFi turns it into a percentage, fills its own 12x8 LED matrix like a gauge, and serves a live dashboard to any phone on your WiFi. No app and no cloud account.
The video covers the R4 WiFi's two chips and why the sketch sends each reply in two prints,
pulseIn's timeout, the median of five pings, drawing on the LED matrix, and the page
that polls /data. The tank, matrix and phone in the video are an animation.
| From | To (UNO R4 WiFi) |
|---|---|
| HC-SR04 VCC | 5V |
| HC-SR04 GND | GND |
| HC-SR04 TRIG | D9 |
| HC-SR04 ECHO | D10 (direct, no divider) |
Libraries: WiFiS3 and Arduino_LED_Matrix (both come with the UNO R4 board package; nothing to install).
// UNO R4 WiFi water tank monitor
// HC-SR04 on the lid -> level on your phone + on the LED matrix
#include "WiFiS3.h"
#include "Arduino_LED_Matrix.h"
#include "arduino_secrets.h" // SECRET_SSID, SECRET_PASS
const int TRIG = 9;
const int ECHO = 10;
const float EMPTY_CM = 100.0; // sensor to the bottom
const float FULL_CM = 15.0; // sensor to the water, full
WiFiServer server(80);
ArduinoLEDMatrix matrix;
uint8_t frame[8][12];
float distCm = NAN;
int level = -1; // percent, -1 = no reading
unsigned long lastRead = 0, lastCheck = 0;
const uint8_t DIGIT[10][5] = { // 3x5 font, one row per byte
{7,5,5,5,7}, {2,6,2,2,7}, {7,1,7,4,7}, {7,1,3,1,7}, {5,5,7,1,1},
{7,4,7,1,7}, {7,4,7,5,7}, {7,1,2,2,2}, {7,5,7,5,7}, {7,5,7,1,7}};
void setup() {
Serial.begin(115200);
pinMode(TRIG, OUTPUT);
pinMode(ECHO, INPUT);
matrix.begin();
if (String(WiFi.firmwareVersion()) < WIFI_FIRMWARE_LATEST_VERSION)
Serial.println("Update the WiFi firmware");
while (WiFi.begin(SECRET_SSID, SECRET_PASS) != WL_CONNECTED)
Serial.println("Retrying WiFi...");
server.begin();
IPAddress ip = WiFi.localIP();
Serial.print("Open http://");
Serial.println(ip);
showNumber(ip[3]); // last part of the address
delay(5000);
}
void loop() {
if (millis() - lastRead >= 2000) {
lastRead = millis();
distCm = medianCm();
if (isnan(distCm)) level = -1;
else level = constrain(percent(distCm), 0, 100);
Serial.print(distCm);
Serial.print(" cm ");
Serial.print(level);
Serial.println(" %");
drawGauge();
}
if (millis() - lastCheck >= 10000) {
lastCheck = millis();
if (WiFi.status() != WL_CONNECTED) {
Serial.println("WiFi lost, reconnecting");
WiFi.begin(SECRET_SSID, SECRET_PASS);
}
}
serve();
}
float readCm() {
digitalWrite(TRIG, LOW);
delayMicroseconds(2);
digitalWrite(TRIG, HIGH);
delayMicroseconds(10);
digitalWrite(TRIG, LOW);
unsigned long us = pulseIn(ECHO, HIGH, 30000); // 30 ms, not the 1 s default
if (us == 0) return NAN;
return us * 0.0343 / 2;
}
float medianCm() { // 5 pings, keep the middle one
float v[5];
int n = 0;
for (int i = 0; i < 5; i++) {
float d = readCm();
if (!isnan(d)) v[n++] = d;
delay(30); // let the last echo die down
}
if (n < 3) return NAN;
for (int i = 1; i < n; i++) // insertion sort
for (int j = i; j > 0 && v[j - 1] > v[j]; j--) {
float t = v[j]; v[j] = v[j - 1]; v[j - 1] = t;
}
return v[n / 2];
}
int percent(float d) { // EMPTY_CM = 0 %, FULL_CM = 100 %
return round((EMPTY_CM - d) * 100 / (EMPTY_CM - FULL_CM));
}
void drawGauge() { // the matrix fills up like the tank
memset(frame, 0, sizeof(frame));
if (level < 0) { // no echo: draw an X
for (int i = 0; i < 8; i++) frame[i][i + 2] = frame[7 - i][i + 2] = 1;
} else {
int rows = (level * 8 + 50) / 100;
for (int r = 0; r < rows; r++)
for (int c = 0; c < 12; c++) frame[7 - r][c] = 1;
}
matrix.renderBitmap(frame, 8, 12);
}
void showNumber(int n) { // up to 3 digits on the matrix
memset(frame, 0, sizeof(frame));
String s = String(n);
int x = 12 - 4 * s.length() + 1;
for (unsigned int k = 0; k < s.length(); k++, x += 4)
for (int r = 0; r < 5; r++)
for (int c = 0; c < 3; c++)
frame[r + 1][x + c] = (DIGIT[s[k] - '0'][r] >> (2 - c)) & 1;
matrix.renderBitmap(frame, 8, 12);
}
const char PAGE[] = R"html(<!DOCTYPE html>
<html><head><meta name="viewport" content="width=device-width">
<title>Tank</title><style>
body{font-family:sans-serif;text-align:center;background:#111;color:#eee}
#tank{width:160px;height:260px;margin:20px auto;border:4px solid #888;
border-top:none;position:relative}
#water{position:absolute;bottom:0;width:100%;background:#2a8cff}
#pct{font-size:56px}
</style></head><body>
<h2>Water tank</h2><div id="tank"><div id="water"></div></div>
<div id="pct">--</div><div id="cm"></div>
<script>
async function tick(){
try{
const d=await (await fetch('/data')).json();
const ok=d.level>=0;
pct.textContent=ok?d.level+' %':'no echo';
water.style.height=(ok?d.level:0)+'%';
cm.textContent=ok?d.cm.toFixed(1)+' cm to the water':'';
}catch(e){pct.textContent='offline';}
}
tick();setInterval(tick,2000);
</script></body></html>)html";
void serve() {
WiFiClient client = server.available();
if (!client) return;
String req = client.readStringUntil('\n'); // "GET /data HTTP/1.1"
while (client.connected()) { // skip the headers
String line = client.readStringUntil('\n');
if (line.length() <= 1) break;
}
if (req.startsWith("GET /data")) {
String cm = String(isnan(distCm) ? 0.0 : distCm, 1);
String body = "{\"level\":" + String(level) + ",\"cm\":" + cm + "}";
reply(client, "application/json", body.c_str());
} else if (req.startsWith("GET / ")) {
reply(client, "text/html", PAGE);
} else {
client.print("HTTP/1.1 404 Not Found\r\nConnection: close\r\n\r\n");
}
client.stop();
}
void reply(WiFiClient &client, const char *type, const char *body) {
String head = "HTTP/1.1 200 OK\r\nContent-Type: ";
head += type;
head += "\r\nConnection: close\r\n\r\n";
client.print(head); // each print = one trip to the WiFi chip,
client.print(body); // so send big pieces, not many small ones
}
#define SECRET_SSID "your-wifi-name"
#define SECRET_PASS "your-wifi-password"
| Symptom | Cause | Fix |
|---|---|---|
| Never connects to WiFi | Wrong name or password, a 5 GHz network, or old WiFi firmware | Check arduino_secrets.h, use a 2.4 GHz network, update the firmware with the Arduino IDE's firmware updater |
| The page won't open | The phone is on mobile data or another network, or the address is wrong | Join the same WiFi and use the full address printed on the Serial Monitor (115200 baud) |
| Readings jump around | The sound cone hits a wall, a pipe or splashes | Mount the sensor clear of the walls, pointing straight down |
| Stuck at 0 % or 100 % | EMPTY_CM or FULL_CM doesn't match the tank, or the water is too close to the sensor | Read the distances on the Serial Monitor and set both numbers again |
| The matrix shows an X | No echo: the sensor is tilted, wired wrong, or out of range | Check TRIG on D9 and ECHO on D10, and that the sensor faces the water |
The UNO R4 WiFi pinout and the HC-SR04's wiring and warnings are free in Robot Maker Toolkit, alongside 10 boards and 56 modules — all offline.
Get it onGoogle Play