Introduction: Ultrasonic Rainwater Tank Capacity Meter

If you are anything like me and have a little bit of an environmental conscience (or are just skinflints eager to save a few bucks - which is also me...), you may have a rainwater tank. I have a tank to harvest the rather infrequent rain we get in Australia - but boy oh boy, when it does rain here, it REALLY rains! My tank stands about 1.5m high and is on a plinth, meaning I need to get out steps to check the water level (or - because I am so lazy, balance precariously on top of an old gas bottle from the BBQ that has now taken up permanent residence as a 'step' beside the tank).

I wanted some way to be able to check the water level in the tank, without all the climbing and hanging onto the drainpipe with one hand (while worrying about what spiders might be behind it - you've heard about Australian spiders - right?)... So, with a renewed late in life interest in electronics, and cheap Arduino clones from China on ebay, I decided to have a go at building a 'widget' to do the job for me.

Now, my 'dream' widget was to be permanently installed in the tank, use a solar charged power source, with a remote readout in my garage, or maybe a wireless transmitter using Bluetooth that I could check from my phone, or perhaps even an ESP type device hosting an automatically updated webpage, so that I could check the level of water in my tank from anywhere in the world over the internet... but really - why do I need all that? So I dialled my grand ideals back a bit (well, quite considerably), and did away with the wirelessness of the solution, the permanently installedness, the solar charging, and the ability to check my tank's level from the back end of beyond (always assuming the back end of beyond has WiFi available, that is...)

The resulting project was downgraded into the hand-held unit seen above, that can be simply held over the opening of the tank and activated by a push button, with a digital readout, that can be read from ground level - far more practical.

Step 1: The Maths...

After toying with several ideas on how to determine the water level - I decided on an ultrasonic transmitter/receiver as the basis for my widget, and using an Arduino to take the readings and do all the maths. The readings returned from the sensor are (indirectly) in the form of a distance - from the ultrasonic sensor to the surface it has bounced off (the water surface - or bottom of the tank, if empty), and back again, so we need to do a few things with this, in order to get to a percentage remaining in the tank.

NB - in actual fact, the value returned from the sensor is really just the time taken for the signal to leave the emitter side and return to the receiver. This is in microseconds - but knowing the speed of sound is 29 microseconds per cm (What? You didn't know that? Pfft...) makes an easy conversion from a time period to a distance measurement.

First - of course, we need to divide the distance by 2 to get the sensor to surface distance. Then, subtract the constant distance from the sensor to the 'max' water depth. The remaining value is the depth of water that has been used. Next, subtract that value from the max water depth, to find the depth of water left in the tank.

This value then, is the basis for any other calculations, such as working out this depth of water as a percentage of the maximum depth, or multplying the depth by the constant 'surface area', to get a volume of water which can be displayed in litres (or gallons, or any other unit - as long as you know the maths to do it - I'm sticking to a percentage for simplicity).

Step 2: Practicalities

The unit could be hand held, but this introduces a small possibility of minor inaccuracies if the unit is not held in the same place, and at the same angle every time. Whilst it would only be a very minor error, and probably not even one that would register, it would be the kind of thing that niggled away at me.

However, being hand-held introduces the much bigger possibility that the damned thing will get dropped into the tank and never seen again. So to mitigate BOTH of these possibilities, it will be fixed onto a length of wood, which is then placed over the tank opening - so that the measurement is taken from the exact same height and angle every time (and if it IS dropped in the tank, at least the wood will float).

A push button activates the unit (thereby eliminating the need for an on/off switch, and the possibility of an accidentally flattened battery), and fires up the sketch in the Arduino. This takes a number of readings from the HC-SR04, and takes the average of them (to mitigate any erratic readings).

I also included a bit of code to check for high or low on one of the Arduino digital I/O pins, and use that to put the unit into what I called 'Calibration' mode. In this mode, the display simply shows the actual distance (divided by 2) returned by the sensor, so I could check the accuracy of it against a tape measure.

Step 3: The Ingredients

The unit consists of three main components...

  1. An HC-SR04 ultrasonic transmitter/receiver module
  2. An Arduino Pro Mini microcontroller
  3. A 4 digit 7 segment LED display or display 'module' such as a TM1637

All of the above can be easily found on ebay, by simply searching for the terms shown in bold print.

In this application, the display simply uses 3 digits to display a % value of 0-100 or 4 digits to show the number of litres (max 2000 in my case), so any 4 digit display will do - you don't have to worry about whether the module has decimal points or colons. A display 'module' (LED mounted on a breakout board, with an interface chip) is easier, as it uses fewer pin connections, but a raw LED display with 12 pins could be accommodated by the Arduino with some small modifications to the code (in fact my original design was based on this setup). Note however, that using a raw LED display also requires 7 resistors to limit current drawn by each segment. I happened to have a TM1637 clock display module available, so decided to use that.

Supplemental bits and bobs include a 9v battery clip (and battery, obviously), a 'push-to-make' momentary push button switch, a project box, header pins, connecting wires, and a length of 2"x4" timber that exceeds the diameter of the tank opening.

The supplemental bits and bobs (apart from the hunk of timber) were purchased from my local hobby electronics outlet chain - which is Jaycar in Australia. I imagine Maplin in the UK would be a viable alternative, and I think there are a few in the US, such as Digikey and Mouser. For other countries, I'm afraid I don't know, but I'm sure that if you lack a suitable high street outlet or online supplier in your country, then Chinese ebay sellers will come through for you, if you don't mind waiting a few weeks for delivery (ironically, despite being one of our closest neighbours, 6 weeks or more is not unusual for delivery to Australia from China!).

Make sure you get a project box that is big enough - I guessed at mine before having the components available, and it is a really really tight squeeze - I may need to get myself a different push button that uses less space.

Oh, and by the way, the length of timber just came from some scrap offcuts I keep in the corner of my garage (as a home for more of those lovely spiders).

Once you understand the scematic and functionality, you may decide to adapt your version, and include an on/off switch, or use an 18650 Li-Ion power source, with solar panel and charge controller to keep it constantly topped up and ready to go, or change the simple LED display for a multi-line LCD or graphical OLED with more information display options, such as showing the percentage AND litres remaining at the same time. Or you may go for the all-singing, all-dancing wireless IoT unit permanenty installed in the tank WITH solar charging. I'd love to hear of your variations and modifications.

Step 4: Testing the Prototype (and Code)

Having purchased the HC-SR04 from a cheap Chinese source on ebay, I wasn't really expecting to receive a hugely accurate unit, so I wanted to test it out on the breadboard first, in case I needed to add some distance correction code into my sketch.

At this point, I was casting around for basic info on how to connect up and use the HC-SR04, and must acknowledge jsvester's instructable "Simple Arduino and HC-SR04 example". His example and experience was a great starting point for me to start coding from.

I found the NewPing library of functions for the HC-SR04, which includes built-in functionality to take the average of multiple readings, thereby making my code a lot simpler.

I found a library for the TM1637 clock display module as well, which made displaying numbers a lot simpler. In my original code (for the 4-digit 7 segment display), I was having to split the number into individual digits, then build each individual digit on the display by knowing which segments to illuminate, and then cycling through each digit in the number, and building that number on the appropriate display digit. This method is called multiplexing, and effectively displays just a single digit at a time, but cycles through them from one digit to the next so quickly, that the human eye doesn't notice, and fools you into believing that all the digits are on at the same time. As with the HC-SR04 library making the measuring operations easier, this display library takes care of all the multiplexing, and digit handling. The Arduino Reference pages linked to above, give some examples, and of course, each library comes with sample code that can be a great help.

So, the pictures above show my test rig - I am testing it on my Arduino Uno for simplicity, as that is already setup for temporary re-usable connections for prototyping. The unit is operating in 'Calibration' mode here (notice that digital pin 10 - the white wire - is connected to ground) and accurately reading 39cm to the box I had randomly placed in front of it, as shown by the tape measure. In this mode, I display the small 'c' ahead of the measurement, just to indicate it is not the normal measurement.

As well as Vcc (5v) and Ground, the HC-SR04 needs 2 other connections - the trigger (yellow to pin 6) and echo (green to pin 7). The display also needs Vcc (5v) and Ground, and 2 more connections - clock (blue to pin 8) and DIO (purple to pin 9). As already mentioned, the operating mode is controlled by a high or low on pin 10 (white). The connections will use the same pins on the Arduino Pro Mini, but will be permanently soldered. The operating mode will be selectable using a jumper across two out of three header pins, connected to Vcc, pin 10, and ground respectively.

The official specs for the HC-SR04 claim something like a maximum error of just 3 millimeters up to the maximum designed operating distance of 4 metres, so imagine my surprise to find that my unit certainly was accurate to that degree up to 2 metres - which is well in excess of what I need. Due to limited space for a quick and dirty test setup, my test results beyond that distance were being corrupted by reflections from surfaces other than my test target, as the beam from the transmitter spread out and took in a wider area. But as long as it is good to 1.5 metres - that'll do me nicely, thank you very much :-)

Step 5: Rainwater Gauge Ino Sketch

The full code is attached, but I will include a few extracts below to explain some of the steps.

First of all, the setup...

#include <TM1637Display.h>
#include <NewPing.h>
#include <math.h>

// pins for HC-SR04
#define pinTrig 6
#define pinEcho 7
NewPing sonar(pinTrig, pinEcho, 155); // 400cms is max for HC-SR04, 155cms is max for tank

// LED Module connection pins (Digital Pins)
#define CLK 8
#define DIO 9
TM1637Display display(CLK, DIO);

// Other pins
#define opMode 10

As well as the TM1637 and NewPing libraries, I have also included a Math library, which gives me access to the 'rounding' function. I use this in some of the maths to allow me to display the percentage to the nearest 5% for example.

Next the pins for the two devices are defined, and the devices initiated.

Finally, I define pin 10 for the operation mode.

// set all segments off for all digits
  uint8_t bytes[] = { 0x00, 0x00, 0x00, 0x00 };

This section of code demonstrates one way to control the display module, allowing individual control of each segment in each digit. I have set the 4 elements in the array called bytes, to all be zero. That means that each bit of each byte is zero. The 8 bits are used to control each of the 7 segments and the decimal point (or the colon in a clock type display). So if all the bits are zero, then none of the segments will be lit. The setSegments operation sends the contents of the array to the display and shows (in this case) nothing. All segments are off.

The most significant bit in a byte controls the DP, and then the remaining 7 bits control the 7 segments from G to A in reverse order. So to display the number 1 for example, requires segments B and C, so the binary representation would be '0b00000110'. (Thanks to for the image above).

  // Take 10 readings, and use median duration.
  int duration = sonar.ping_median(10);  // duration is in microseconds
  if(duration == 0)   // Measuring Error - inconclusive or no echo
    uint8_t bytes[] = { 0x00, 0b01111001, 0b01010000, 0b01010000 }; // Segments to spell "Err"

Here, I am telling the HC-SR04 to take 10 readings, and give me the average result. If no value is returned, then the unit is out of range. I then use the same technique as above to control specific segments on the 4 digits, to spell out the letters (blank), E, r, and r. Using binary notation makes it a little easier to relate the individual bits to the segments.

Step 6: Loading Code to an Arduino Pro Mini (without USB)

As I said earlier, items from Chinese ebay sellers often take 6 weeks or more to arrive, and a lot of my prototyping and code writing was done while waiting for some of the components to arrive - the Arduino Pro Mini being one of them.

One thing I didn't notice about the Pro Mini, until I had already ordered it, is that it doesn't have a USB port on it for downloading the sketch. So, after some frantic googling, I found that there are two ways to load a sketch in this case - one requires a special cable that goes from the USB on your PC, to 6 specific pins on the Pro Mini. This group of 6 pins are known as the ISP (in-system programmer) pins, and you can actually use this method on any Arduino if you wanted to - but as the USB interface is available on pretty much all the other Arduino variants (I think), using that option is much simpler. The other method requires you to have another Arduino with a USB interface on it, to act as a 'go-between'.

Luckily, having my Arduino Uno meant that I could use the second method, which I will outline for you below. It is called using the 'Arduino as ISP'. In a nutshell, you load a special sketch onto your 'go-between' Arduino, that turns it into a Serial Interface. Then load your actual sketch, but instead of the normal upload option, you use an option from the IDE menu that uploads 'using the Arduino as ISP'. The 'go-between' Arduino then takes your actual sketch from the IDE, and passes it on to the ISP pins of the Pro Mini, rather than loading it to its own memory. It isn't difficult once you get your head around how it works, but it is an extra layer of complexity that you might want to avoid. If that's the case, or you don't have another Arduino you can use as the 'go-between', then you might want to buy an Arduino Nano, or one of the other small form factor models, that does include the USB interface and makes programming a simpler prospect.

Here are a couple of resources that you might find helpful in understanding the process. The Arduino Reference is specifically referring to burning a new bootloader to the target device, but you can just as easily load a sketch in the same way. I found Julian Ilett's video makes the concept a lot clearer, though he skips the part in the Arduino reference that explains how to wire the two Arduinos together, and programs a bare chip on a breadboard instead.

As the Pro Mini does not have the 6 ISP pins conveniently grouped together, you need to decode which of the digital pins relate to the 4 programming pins (the other two connections are just Vcc and Gnd - so are pretty straightforward). Luckily for you, I've already been through this - and am willing to share the knowledge with you - what a generous person I am!!

The Arduino Uno, and many others in the Arduino family, have the 6 pins handily arranged in a 3x2 block, like this (image from

Unfortunately, the Pro Mini doesn't. As you can see below, they are actually quite easy to identify and are still arranged in 2 blocks of 3 pins. MOSI, MISO, and SCK are the same as digital pins 11, 12, and 13 respectively on both the Pro Mini and Arduino Uno, and for ISP programming, simply connect 11 to 11, 12 to 12, and 13 to 13. The Pro Mini's Reset pin should be connected to the Uno pin 10, and the Pro Mini's Vcc (5v)/Ground should be connected to the Arduino +5v/Ground. (Image from

Step 7: Assembly

As I mentioned, I took a punt on the case, and regretted it. To fit all the components in was a real squeeze. In fact I had to bend the push button contacts out sideways, and put some packing on the outside to lift it a bit further so that it would fit in the depth of the box, and I had to grind 2-3mm off each side of the display module board for it to fit too.

I drilled 2 holes in the case for the ultrasonic sensors to poke through. I drilled the holes a little too small and then gradually increased them using a small rotary grinder, so I could get them to be a nice 'push fit'. Unfortunately, they were too close to the sides to be able to use the grinder from inside the box, and this had to be done from outside, resulting in many scratches and skate marks where the grinder slipped - oh well, that's all on the bottom anyway - who cares..?

I then cut a slot in one end that is the right size for the display to poke through. Again - my guess on the box size bit me on the rear as the slot left me with a very slim piece above the display, which inevitably broke while I was filing it smooth. Oh well - that's what super-glue was invented for...

Finally, with all the components roughly positioned in the box, I measured where to put the hole in the lid, so that the body of the push button would fall into the final available space. JUST!!!

Next, I soldered all the components together to test they all still worked after my bending and grinding and trimming, before assembling them all into the case. You can see the jumper connection just below the display module, with pin 10 on the Arduino (white lead) connected to Gnd, thus putting the unit in calibration mode. The display reads 122cms up from my bench - it must have picked up a signal reflected back from the top of the window frame (its too low to be the ceiling).

Then it was a case of breaking out the hot glue gun, and shoe-horning all the components into place. Having done that, I found that the tiny clearance between the top of the display module and the lid, once the module was glued in place, left a bit of a bulge where the lid won't quite fit as snugly as I'd like. I might try and do something about that one day - or more likely, I won't...

Step 8: The Finished Article

After a few post-assembly tests, and a correction to my code to account for the depth of the chunk of wood I'd screwed the device to (which I completely overlooked in my calculations - d'oh!!), it's all done. Finally!

Assembled testing

With the unit just sitting face down on my bench, obviously there will be no reflected signal, so the unit correctly shows an error condition. The same would be true if the closest reflecting surface is beyond the unit's range.

Looks like from my bench top to the floor is 76cms (well, 72cms plus the 4cm depth of the chunk of wood).

The underside of the unit, showing the transmitter and receiver overhanging the chunk of wood - I should really stop calling it a chunk of wood - it will henceforth be referred to as the Gauge Stabilisation and Precision Placement Platform! Thankfully, this is probably the last time I'll mention it ;-)

Ooh - you can see all those nasty scratches and skate marks in this one...

... and here is the finished item, placed in normal operating mode, actually measuring the capacity of my tank to the nearest 5%. It was a (very) rainy Sunday afternoon that saw me finish this project off, hence the raindrops on the unit, and the very pleasing 90% reading.

I hope that you've enjoyed reading this instructable, and that you learned a little about Arduino programming, physics and the use of sonar/ultrasonic reflection, the pitfalls of using guesswork in your project planning, and that you have been inspired to make your own rainwater tank gauge - and then to install a rainwater tank to use it on, while helping the environment a little and saving on your water bill.

Please read on - for what happened the next day...!

Step 9: Postscript - One Hundred (and Five) Percent?

So, on the Monday after the rainy Sunday, the tank was absolutely as full as it could possibly be. As it is one of the very few times I have ever seen it completely full, I thought it would be the ideal time to benchmark the gauge, but guess what - it registered as 105%, so there was obviously something wrong.

I got out my dipstick and found that my original assumptions of 140cms as the maximum depth of water, and 16cms of headroom (based on visual guesstimates made from outside the tank), were both a little off the actual measurements. So armed with the real data for my 100% benchmark, I was able to tweak my code and reload the Arduino.

The maximum water depth turns out to be 147cms, with the measuring point sitting at 160cms, giving 13cms of headroom (the sum of the headroom within the tank, the height of the neck of the tank, and the depth of the chunk of... whoa, no, what?! I mean depth of the Gauge Stabilisation and Precision Placement Platform!).

After correcting the maxDepth and headroom variables accordingly, as well as resetting the maximum range of the sonar object to be 160cms, a quick retest showed 100% that dropped to 95% as I lifted the gauge a little (to simulate a small amount of the water having been used).

Job done!

PS - this is my first attempt at an instructable. If you like my style, sense of humour, honesty to admitting mistakes (hey - even I am not perfect...), etc - let me know and it may give me the boost to do another one.

Step 10: Afterthoughts

Usable Capacity

So it has been a few weeks now since I published this Instructable, and I have had many comments in response, some of which have been suggesting some alternative mechanisms - both electronic and manual. But this got me thinking, and there is something I should probably have pointed out in the beginning.

  • My tank has a pump, which is installed at ground level - just slightly below the base of the tank. As the pump is the lowest point in the system, and the water from the pump is under pressure, I can use the full capacity of my tank.
  • HOWEVER - if your tank does not have a pump, and relies on gravity feed, then the effective capacity of the tank is limited by the height of your tap. Once the water remaining in your tank is lower than the tap, then no water will flow.

So, regardless of whether you are using an electronic gauge, or a manual sight glass, or float and flag type system, just be aware that without a pump, the effective 'base' of your tank is actually the height of the tank's outlet or tap.

Epilog Challenge 9

Participated in the
Epilog Challenge 9

First Time Author Contest 2018

Participated in the
First Time Author Contest 2018