Robot Maker Toolkit › UNO R4 WiFi tank monitor

UNO R4 WiFi water tank monitor

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.

Beginner+ · ~30 min Arduino UNO R4 WiFi No libraries to install
✔ HC-SR04 pins match the app ✔ Compiles for the UNO R4 WiFi ✔ Web page tested in a browser ◻ Bench-tested — be the first, get credited

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.

Parts

Wiring

FromTo (UNO R4 WiFi)
HC-SR04 VCC5V
HC-SR04 GNDGND
HC-SR04 TRIGD9
HC-SR04 ECHOD10 (direct, no divider)

Before you wire it

The sketch

Libraries: WiFiS3 and Arduino_LED_Matrix (both come with the UNO R4 board package; nothing to install).

r4_tank.ino (copy everything)
// 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
}
arduino_secrets.h (a second tab in the IDE)
#define SECRET_SSID "your-wifi-name"
#define SECRET_PASS "your-wifi-password"

If it doesn't work

SymptomCauseFix
Never connects to WiFiWrong name or password, a 5 GHz network, or old WiFi firmwareCheck arduino_secrets.h, use a 2.4 GHz network, update the firmware with the Arduino IDE's firmware updater
The page won't openThe phone is on mobile data or another network, or the address is wrongJoin the same WiFi and use the full address printed on the Serial Monitor (115200 baud)
Readings jump aroundThe sound cone hits a wall, a pipe or splashesMount 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 sensorRead the distances on the Serial Monitor and set both numbers again
The matrix shows an XNo echo: the sensor is tilted, wired wrong, or out of rangeCheck 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
Board pinout library →