Water Tank Level Indicator with Low Level Warning Notifications

Hi All,

After a few years Blynking now I thought I might share my water tank project with the Blynk Community. I have a 9000 litre water tank and I have used the following items.

  1. ESP8266
  2. JSN-SR04T (ver1.0) water proof ultrasonic. Please note that that a minimum distance of about 20cm is needed for accurate measurement to commence.
  3. 5v relay.

Main features are a graphical water level indicator, super graph showing historical water level and a level variable water level alert (push notification) which can be switched on or off. The push notification will come every 2 minutes until the level is adjusted or the alert is cancelled.

The code maybe a bit messy but it does work. Please let me know your thoughts.

EDITED: Improved formula for tank volume and added a smoothing filter (credit to Paul Badger - 2007)

//****Virtual Pin Assignments****

// V0 - Blynk Version
// V1 - ESP Version
// V2 - Millis Uptime
// V3 - Wifi RSSI
// V4 - Volume
// V8 - Terminal
// V9 - Percent (level V widget)
//V10 - Numeric Widget - Level Alert
//V11 - Button Widget to cancel alert or turn alert (on or off)
//V12 - Distance - height from sensor to water level

//#define BLYNK_PRINT Serial    // Comment this out to disable prints and save space

#include <ESP8266WiFi.h>
#include <BlynkSimpleEsp8266.h>

#include <ESP8266mDNS.h>  // For OTA w/ ESP8266
#include <WiFiUdp.h>  // For OTA
#include <ArduinoOTA.h>  // For OTA

char auth[] = "*****";

char ssid[] = "*****";
char pass[] = "****";

BlynkTimer timer;

WidgetTerminal terminal(V8);

#define echoPin 14// Echo Pin
#define trigPin 12  // Trigger Pin

#define filterSamples   15              // filterSamples should  be an odd number, no smaller than 3

int maximumRange = 9000;
int minimumRange = 20;
int alertLevelCounter = 0; //Counter starts at zero
int volume;
int volume1; // for smoothing algorithum
int percent;
int percent1; // for smoothing algorithum
int levelAlert;
int wateralert = 0;
int sensSmoothArray1 [filterSamples];   // array for holding raw sensor values for sensor1 
int smoothDistance;                     // variables for sensor1 data

double alertInMin = 0.5;                //Alert time following alert water level being reached (0.5 = 30secs)

const int alertDuration = 2;            //Alert sent every 2 minutes

const int waterTankDia = 253;           //Water Tank Diameter (cm)
const int waterTankHt = 202;            //ie. Sensor Height (cm)


long duration, distance, distance1;     // Duration used to calculate distance


BLYNK_CONNECTED() { // runs once at device startup, once connected to server.
  Blynk.syncVirtual(V10);
  Blynk.syncVirtual(V11);
}
 
void MeasureCm() {
 //  The following trigPin/echoPin cycle is used to determine the
 // distance of the nearest object by bouncing soundwaves off of it. 
 
 digitalWrite(trigPin, LOW); 
 delayMicroseconds(2); 

 digitalWrite(trigPin, HIGH);
 delayMicroseconds(10); 
 
 digitalWrite(trigPin, LOW);
 duration = pulseIn(echoPin, HIGH);
 
 //Calculate the distance (in cm) based on the speed of sound.
 distance = duration/58.2;
 
 {
 
   if(distance >= 20 ){
 volume = ((waterTankHt*(PI*(sq(waterTankDia/2))))-(distance*(PI*(sq(waterTankDia/2)))))/1000;
 percent = ((float)volume/9000)*100;
  
   } 
 else{
     if(distance < 20 ){
       volume = 9200;
       percent = 100;
     }
 }
 }

  Blynk.virtualWrite(V9, percent); // virtual pin
  Blynk.virtualWrite(V12, distance); // virtual pin
  Blynk.virtualWrite(V4, volume); // virtual pin
} 

void UpTime() {
  Blynk.virtualWrite(2, (millis() / 1000));
  Blynk.virtualWrite(3, WiFi.RSSI());
  //terminal.println(levelAlert);
}


BLYNK_WRITE(V10) {
  levelAlert = param.asInt();
  terminal.print("Water Level Alert: ");
  terminal.println(levelAlert);
  terminal.println();
  terminal.flush();
} 
   
BLYNK_WRITE(V11) { 

  int alertOff = param.asInt();

  if(alertOff == 0) {
resetWaterLevelAlert();
wateralert = 1;
terminal.print("Level Alert is OFF");
terminal.println();
terminal.flush();
  }
  else {
resetWaterLevelAlert();
wateralert = 0;
terminal.print("Level Alert is ON");
terminal.println();
terminal.flush();
  }

}


void checkWaterLevel() {
  
  if(volume1 < levelAlert && wateralert == 0){
 alertLevelCounter += 1;
  }

  if(alertLevelCounter > alertInMin * 60 && wateralert == 0){
Blynk.notify("Water Tank Level Alert"); // send push notification to blynk app
wateralert = 1;
resetWaterLevelAlert();
  }
}

void resetWaterLevelAlert() {
  alertLevelCounter = 0;
  wateralert = 0;
}


void MeasureCmForSmoothing() {
  /* The following trigPin/echoPin cycle is used to determine the
 distance of the nearest object by bouncing soundwaves off of it. */ 

 digitalWrite(trigPin, LOW); 
 delayMicroseconds(2); 

 digitalWrite(trigPin, HIGH);
 delayMicroseconds(10); 
 
 digitalWrite(trigPin, LOW);
 duration = pulseIn(echoPin, HIGH);

 
 //Calculate the distance (in cm) based on the speed of sound.
 
 distance1 = duration/58.2; //for smoothing algorithim

 smoothDistance = digitalSmooth(distance1, sensSmoothArray1);

 {
 
 if(distance1 >= 20 ) {
   volume1 = ((waterTankHt*(PI*(sq(waterTankDia/2))))-(smoothDistance*(PI*(sq(waterTankDia/2)))))/1000;
   percent1 = ((float)volume1/9000)*100;
   terminal.print("Uptime (secs): ");
   terminal.println((millis() / 1000));
   terminal.print("Distance: ");
   terminal.println(distance1);
   terminal.print("Smoothed Distance: ");
   terminal.println(smoothDistance);
   terminal.flush();
   //terminal.print("Percent: ");
   // terminal.println(percent1);
 } 
   else{
    if(distance1 < 20 ){
      volume1 = 9200;
      percent1 = 100;
    }
   }  
 }
} 

void UploadMeasureCmForSmoothing() {
Blynk.virtualWrite(V12, smoothDistance); // virtual pin
Blynk.virtualWrite(V4, volume1); // virtual pin
Blynk.virtualWrite(V9, percent1); // virtual pin
  
terminal.print("Uploaded Volume: ");
terminal.println(volume1);
terminal.print("Uploaded Smoothed Distance: ");
terminal.println(smoothDistance);
terminal.println();
terminal.flush();
//terminal.print(sorted[j]); 
//terminal.println("   "); 
//Serial.println();
//terminal.print("average = ");
//terminal.println(total/k);
//terminal.print("Uploaded Percent: ");
//terminal.print(percent1);
}

  

void setup() {
 Serial.begin(115200);
 Blynk.begin(auth, ssid, pass);

 ArduinoOTA.setHostname("Water_Tank");  // For OTA - Use your own device identifying name
 ArduinoOTA.begin();  // For OTA

 Blynk.virtualWrite(0, BLYNK_VERSION);
 Blynk.virtualWrite(1, ESP.getCoreVersion());

 pinMode(trigPin, OUTPUT);
 pinMode(echoPin, INPUT);

 terminal.clear();
 
 timer.setTimeout(100, [](){
 timer.setInterval(5000L, UpTime);
 });

 timer.setTimeout(400, [](){
 timer.setInterval(300000L, UploadMeasureCmForSmoothing);  //get a measurement straight up if the device was reset for some reason
 });

 timer.setTimeout(600, [](){
 timer.setTimer(1000L, MeasureCm, 1);
 });
  
 timer.setTimeout(500, [](){
 timer.setInterval(1000L, checkWaterLevel);
 });

 timer.setTimeout(700, [](){
 timer.setInterval(15000L, MeasureCmForSmoothing);
 });

}
 
void loop() {
  Blynk.run(); // Initiates Blynk
  ArduinoOTA.handle();  // For OTA
  timer.run(); // Initiates SimpleTimer
}

int digitalSmooth(int rawIn, int *sensSmoothArray){     // "int *sensSmoothArray" passes an array to the function - the asterisk indicates the array name is a pointer
int j, k, temp, top, bottom;
long total;
static int i;
// static int raw[filterSamples];
static int sorted[filterSamples];
boolean done;

i = (i + 1) % filterSamples;    // increment counter and roll over if necc. -  % (modulo operator) rolls over variable
sensSmoothArray[i] = rawIn;                 // input new data into the oldest slot

// Serial.print("raw = ");

 for (j=0; j<filterSamples; j++){     // transfer data array into anther array for sorting and averaging
     sorted[j] = sensSmoothArray[j];
 }

   done = 0;                // flag to know when we're done sorting              
 while(done != 1){        // simple swap sort, sorts numbers from lowest to highest
      done = 1;
        for(j = 0; j < (filterSamples - 1); j++){
           if(sorted[j] > sorted[j + 1]){     // numbers are out of order - swap
             temp = sorted[j + 1];
             sorted [j+1] =  sorted[j] ;
             sorted [j] = temp;
             done = 0;
           }
        }
  }

/*
  for (j = 0; j < (filterSamples); j++){    // print the array to debug
Serial.print(sorted[j]); 
Serial.print("   "); 
  }
  Serial.println();
*/
   // throw out top and bottom 15% of samples - limit to throw out at least one from top and bottom
   //  bottom = max(((filterSamples * 15)  / 100), 1); 
   //  top = min((((filterSamples * 85) / 100) + 1  ), (filterSamples - 1));   // the + 1 is to make up for asymmetry caused by integer rounding
   bottom = max(((filterSamples * 20)  / 100), 1); 
   top = min((((filterSamples * 80) / 100) + 1  ), (filterSamples - 1));   // the + 1 is to make up for asymmetry caused by integer rounding
   k = 0;
   total = 0;
  for( j = bottom; j< top; j++){
 total += sorted[j];  // total remaining indices
 k++; 
 //Serial.print(sorted[j]); 
 //Serial.print("   "); 
 //terminal.print(sorted[j]); 
 //terminal.println("   ");
  
  }
   //terminal.print("average: ");
   //terminal.println(total/k);
   //terminal.flush();
   //Serial.println();
   //Serial.print("average = ");
   //Serial.println(total/k);
  return total / k;    // divide by number of samples
}

12 Likes

Nice project!.. thanks for the post :smiley:

Interesting use of lambda timeout timers for staggering the interval timers startup. Since they already startup at staggered times, was this for any other particular reason?

And FYI this can be done without needing a timer by using this Blynk function.

BLYNK_CONNECTED() {  // runs once at device startup, once connected to server.
    MeasureCm(); 
}

Thanks @Gunner. Good point about the timers…originally I had all the timers checking every second hence the reason for the staggered start. I will tidy this up thanks.

Also thanks for the tip about the blynk connected. I didn’t realise it could be done this way.

Another FYI… even if you did need all the timers running in such short intervals, this would start (and run) them in a staggered format… with (possibly) enough time to complete each’s respective function before the next timer started.

timer.setInterval(1000L, UpTime);  // Starts and runs every second
timer.setInterval(1030L, MeasureCm);   // Starts and runs 30ms after first timer
timer.setInterval(1070L, checkWaterLevel);  // Starts and runs 70 ms after first timer & 40ms after 2nd timer
3 Likes

Thanks again @Gunner.

@GG07
Hi GG07 , great project.
i want to ask, how you measured volume of the water tank.can you explain me the numbers ?
i want adopt this project for a cisterne (7000L, almost a cylinder)

Because the sensor has a minimum measurement of about 20cm, I had to add this measurement to the maximum water height my tank and work out the volume accordingly.

Refers to volume of a cylinder- pi * (tank radius * tank radius)* water level.

I will tidy the code up by declaring these figures globally eventually.

I did find that with this sensor every now and then I would get a crazy reading so I have added some code to smooth this issue based on Paul Badger’s (2007) digital smooth code https://playground.arduino.cc/Main/DigitalSmooth
I will repost my code with this feature added if you like?

@GG07 that would be great !thanks

@GG07

i guess , these are cm?

I dont get this number :sweat_smile:
what unit is this and how di you get to this number?
i have a 7000L cisterne , when its full , the are around 30 cm to the water level.

Ok…so my tank dimensions are

253cm = diameter (radius = 126.5cm)
202cm = tank height (also sensor height)

Vol = (pi * r^2)* tank height
= (3.1415926536 * (126.5 * 126.5))* 202
= 10,155,055.310263

So the calculations indicate a 10,155 litre capacity but the tank overflow is set at 9000litres.

Is your tank round?

yes , its an up standing cilinder ,approximately
160cm diameter
340cm tank height
@GG07 now i`m testing the code with a 17L bucket . so far so good. what do i have to chance in the code , if the sensor is 30cm above the bucket ?
idont know , if the is a pm funktion in this forum, but you can mail me : niklas.hallmann@yahoo.de

If the sensor is 30cm above the bucket then just add the 30cm to the height of the bucket then work out the volume based on that measurement.

1 Like

thx @GG07 .now it works ! i also managed it to send the values to thinkspeak .
now i want to implement a 0.96 Inch OLED I2C Display.
maybe you can help me with that too.

That’s good to hear. I haven’t done much with the OLED screens. There’ll be something on the Blynk forum to help you though.

@Xrapto92…I’ve updated my code with an improved formula for tank volume and added a smoothing filter. How did you go with your OLED?

very interesting project , could you please show how the sensor installed in the tanks , and if you have any pictures to share that will be great.

thanks

1 Like

Sorry for bumping up an old question but I’m in the same boat of requirement.

And to my surprise there are quite a few new sensors in town:

This one has 2 transducers instead of 1 therefore has less minimum detection distance as compared to JSN SR04T. It is also completely sealed and waterproof.

https://www.ebay.com/itm/253978673298

Before finalizing on this I had bought another one which was a waterproof modified version of HC-SR04 from here

https://www.ebay.com/itm/254113850508

The 1st one is much more rugged and waterproof as compared to the latter one.

1 Like

That’s great mate. Yeah that minimum distance of around 23cm for the JSN sensor is a little painful… Thanks for sharing. You have purchased and tested the waterproof ones? Performance results?

Yes I did purchase both of them and tried and tested both of them for a personal project of mine of a water tank level controller. I wanted the water to be of potable/drinking quality, therefore I could not use float switches, the 1st sensor was like a savior because of the JSN SR04T minimum distance problem.

Personally I would say the sensor in the 1st link (https://www.ebay.com/itm/253978673298) is far better than the sensor in the 2nd one in terms of ruggedness and exposure to humid/water environments. It does have minimum distance of around 3-4cm and its working fine since a couple of weeks. It even has 2 screw mounting holes on either sides which helps to connect it to the box which contains the micro-controller (using and ESP32 btw) and power supply and stuff.

1 Like

Hi Guys,

I#m struggeling with activation my JSN-SR04t with the Blynk code.

I see:

#define echoPin 14// Echo Pin
#define trigPin 12  // Trigger Pin

I think here i have to define my Ports in the Wemos D1 Mini. I have connected the trigger to D3 and the Echo to D4 on the board. What do i have to code here?

thanks for help

best Jens