Introduction: Battery-powered Water Collector Level Sensor

Our house has a water tank fed from the rain falling on the roof, and used for the toilet, the washing machine and watering plants in the garden. For the last three years the summers were very dry, so we kept an eye on the water level in the tank. So far, we used a wooden stick, which we put in the tank and marked the level. But surely it must be possible to improve on this!

This is where this project comes in. The idea is to attach an ultrasonic distance sensor at the top of the tank. This sensor works as a sonar emitting sound waves, which are then reflected by the water surface. From the time it takes for the waves to come back and the speed of sound, you can calculate the distance to the water surface and determine how full the tank is.

Since I don't have a mains connection close to the tank it is essential that the complete device works on batteries. This means I had to be conscious about the power consumption of all the parts. To send back the data I decided to use the built-in Wifi of an ESP8266 microchip. While the Wifi is fairly power-hungry, it has an advantage over another type of radio connection: you can directly connect to your home's wireless router without having to build another device that acts as a relay.

To save power I'll put the ESP8266 in deep sleep most of the time and take a measurement every hour. For my purpose of following up on the water level this more than suffices. The data will be send to ThingSpeak and can then be read off on a smartphone through an app.

One more detail! The speed of sound, essential for the distance measurement, depends on the temperature and to a lesser extent on the humidity. For an accurate outside measurement over the seasons we'll thrown in a BME280 sensor, which measures temperature, humidity and pressure. As a bonus this makes from our water level sensor also a mini weather station.


I made the code available on GitHub.

Step 1: Getting to Know the Ultrasonic Distance Sensor

We'll measure the distance to the water surface with an ultrasonic sensor, the HC-SR04-P. Just like a bat, this sensor uses sonar: it sends a sound pulse with a frequency too high for the human ear, hence ultrasonic, and waits for it to hit an object, reflect and come back. The distance can then be calculated form the time it takes to receive the echo and the speed of sound.

Concretely, if the Trig pin is pulled high for at least 10 μs the sensor sends a burst of 8 pulses with a frequency of 40 Hz. The answer is then obtained on the Echo pin in the form of a pulse with duration equal to the time between sending and receiving the ultrasonic pulse. Then we have to divide by 2, since the ultrasonic pulse is going back and forth and we need the one-way travel time, and multiply by the speed of sound, which is about 340 m/s.

But wait a minute! In fact, the speed of sound depends on the temperature and to a lesser extent on the humidity. Am I nitpicking or is this relevant? Using a calculation tool we find that in the winter (taking -5 °C) we could have 328.5 m/s, and in the summer (taking 25 °C) 347.1 m/s. So suppose we find a one-way travel time of 3 ms. In winter, this would mean 98.55 cm and in summer 104.13 cm. That's quite a difference! So to obtain enough accuracy throughout the seasons and even day and night we have to add a thermometer to our setup. I decided to include the BME280, which measures temperature, humidity and pressure. In the code I used in the function speedOfSound a formula that calculates the speed of sound in terms of all three parameters, although the temperature is really the most important factor. The humidity still has a smaller effect, but the impact of the pressure is negligible. We could use a simpler formula taking into account only the temperature which I implemented in speedOfSoundSimple.

There is one further important point on the HC-SR04. There are two versions available: the standard version operates at 5V, while the HC-SR04-P can operate at a range of voltages from 3V to 5V. Since the 3 rechargeable AA batteries provide around 3x1.25V=3.75V it is important to get the P-version. Some sellers might send the wrong one. So take a look at the pictures if you buy one. The two versions look different both at the back and at the front as explained on this page. At the back on the P-version all three chips are horizontal while on the standard version one is vertical. At the front the standard version has an extra silver component.

In the electronic circuit we'll use a transistor as a switch to turn off the power to the ultrasonic sensor when our setup goes to deep sleep to save battery life. Otherwise, it would still consume about 2mA. The BME280 on the other hand only consume about 5 μ when inactive, so it is not necessary to switch it off with the transistor.

Step 2: Choice of the ESP8266 Board

To operate the sensor as long as possible on a battery we have to economize on power consumption. While the Wifi of the ESP8266 provides a very convenient way to connect our sensor to the cloud, it is also quite power-hungry. In operation the ESP8266 consumes about 80mA. So with batteries of 2600 mAh we would only be able to run our device for at most 32 hours before they are empty. In practice, it'll be less since we'll not be able to use the full 2600 mAh capacity before the voltage drops to a too low level.

Luckily the ESP8266 also has a deep-sleep mode, in which almost everything is off. So the plan is to put the ESP8266 in deep sleep most of the time and wake it up ever so often to make a measurement and send the data over the Wifi to ThingSpeak. According to this page the max deep-sleep time used to be about 71 minutes, but since the ESP8266 Arduino core 2.4.1 it has increased to about 3.5 hours. In my code I settled for one hour.

I first tried the convenient NodeMCU development board, but bummer, in deep sleep it still consumed about 9 mA, which gives us at most 12 days of pure deep sleep without even considering the wake-up intervals. An important culprit is the AMS1117 voltage regulator, which uses power even if you try to bypass it by connecting the battery directly to the 3.3V pin. This page explains how to remove the voltage regulator and the USB UART. However, I never managed to do that without destroying my board. Moreover, after removing the USB UART you can't connect to the ESP8266 anymore to figure out what went wrong.

Most ESP8266 development boards seem to use the wasteful AMS1117 voltage regulator. One exception is the WEMOS D1 mini (picture on the left) which comes with the more economical ME6211. Indeed, I found that the WEMOS D1 mini uses about 150 μA in deep sleep, which is more like it. Most of it is probably due to the USB UART. With this board you have to solder the headers for the pins yourself though.

However, we can do much better using a bare-bones board like the ESP-12F (picture on the right), which doesn't have an USB UART or a voltage regulator. Feeding the 3.3V pin I found a deep-sleep consumption of only 22 μA!

But to get the ESP-12F to work prepare for some soldering and a bit more hassle programming it! Further unless the batteries directly deliver the correct voltage, which is between 3V and 3.6V, we need to provide our own voltage regulator. In practice, it turns out to be difficult to find a battery system that provides a voltage in this range over its full discharge cycle. Remember we also need to power the HC-SR04-P sensor, which theoretically can function with a voltage as low as 3V, but does function more accurately if the voltage is higher. Moreover in my diagram the HC-SR04-P is turned on by a transistor, which induces a small extra voltage drop. We will use the MCP1700-3302E voltage regulator. The maximum input voltage is 6V so we feed it with up to 4 AA batteries. I decided to use 3 AA batteries.

Step 3: Create a ThingSpeak Channel

We'll use ThingSpeak, an IoT cloud service, to store our data. Go to and create an account. Once you are logged in click the button New Channel to create a channel. In the Channel Settings fill in the name and description as you like. Next we name the channel fields and activate them by clicking the checkboxes to the right. If you use my code unchanged the fields are as follows:

  • Field 1: water level (cm)
  • Field 2: battery level (V)
  • Field 3: temperature (°C)
  • Field 4: humidity (%)
  • Field 5: pressure (Pa)

For future reference write down the Channel ID, the Read API Key and the Write API Key, which can be found in the menu API keys.

You can read off the ThingSpeak data on your smartphone using an app. On my android phone I use the IoT ThingSpeak Monitor widget. You have to configure it with the Channel ID and the Read API Key.

Step 4: How to Program the ESP-12F

We need a bare-bones board to save on battery life, but the downside is that it is a bit more difficult to program than a development board with built-in USB UART.

We'll use the Arduino IDE. There are other Instructables explaining how to use it so I'll be brief here. The steps to make it ready for the ESP8266 are:

To handle the ESP-12F I used an adapter plate, commonly available in online shops. I soldered the chip to the plate and then soldered the headers to the plate. Only then I discovered that the adapter plate is too wide for a standard breadboard! It leaves no free pins on the side to make your connections.

The solution I went for is to use U-shaped wires and connect them as in the picture on the right before putting the ESP8266 with the adapter plate on the breadboard. So GND and VCC are connected to the rails of the breadboard and the remaining pins are made available further down the breadboard. The disadvantage is that your breadboard is going to be pretty crowded with wires once you finish the complete circuit. Another solution is to fit two breadboards together as shown in this video.

Next, to program the ESP-12F through the USB-port of your computer we need a USB to serial adapter. I used the FT232RL FTDI programmer. The programmer has a jumper to select between 3.3V or 5V. It should be put to 3.3V for the ESP8266. Don't forget it as 5V might fry your chip! The installation of the drivers should be automatic, but if the programming does not work you can try to manually install them from this page.

The ESP8266 has a programming mode to upload new firmware to the flash, and a flash mode to run the current firmware from the flash memory. To choose between these modes some pins must take a certain value at boot time:

  • Programming: GPIO0: low, CH-PD: high, GPIO2: high, GPIO15: low
  • Flash: GPIO0: high, CH-PD: high, GPIO2: high, GPIO15: low

The adapter plate already takes care of pulling up CH-PD and pulling down GPIO15 with 10K resistors.

So in our electronic circuit we still need to pull-up GPIO2. We also provide a switch to put the ESP8266 in programming or in flash mode and a switch to reset it, which is done by connecting RST to ground. Further make sure you connect the TX pin of the FT232RL to the RXD pin of the ESP8266 and vice-versa.

The programming sequence is as follows:

  • Set GPIO2 to low by closing the programming switch.
  • Reset the ESP8266 by closing and then reopening the reset switch. The ESP8266 now boots in programming mode.
  • Set GPIO2 back to high by opening the programming switch.
  • Upload the new firmware from the Arduino IDE.
  • Reset the ESP8266 again by closing and reopening the reset switch. The ESP8266 now boots in flash mode and runs the new firmware.

Now you can test whether the programming works by uploading the famous Blink sketch.

If all this works at least the GND, VCC, GPIO2, RST, TXD and RXD pins are correctly soldered and connected. What a relief! But before proceeding I would recommend to also test the other pins with your multimeter. I had a problem myself with one of the pins. You can use this sketch, which sets all the pins to high one by one for 5 seconds, and afterwards puts the ESP8266 in deep sleep for 20 seconds. To enable the ESP8266 to wake up after deep sleep you need to connect RST to GPIO16, which gives the wake signal.

Step 5: Uploading the Sketch

I have made the code available on GitHub, it's just one file: Level-Sensor-Deepsleep.ino. Just download it and open it in the Arduino IDE. Or you can select File - New and just copy/paste the code.

There is some info you have to fill in the beginning of the file: the name and password of the WLAN to use, static IP details and the Channel ID and Write API Key of the ThingSpeak Channel.

Following the tip on this blog, instead of DHCP where the router dynamically assigns an IP, we use static IP, where we set the IP address of the ESP8266 ourselves. This turns out to be much faster, so we save on active time and thus on battery energy. So we have to provide an available static IP address as as well as the IP of the router (gateway), the subnet mask and a DNS server. If you are unsure about what to fill in, read about setting up a static IP in the manual of your router. On a Windows computer connected through Wifi to you router, start a shell (Windows button-r, cmd) and enter ipconfig /all. You will find most of the info you need under the Wi-Fi section.

Examining the code you see that unlike other Arduino code most of the action happens in the setup function instead of the loop function. This is because the ESP8266 goes to deep sleep after it finishes the setup function (unless we started in OTA mode). After it wakes up, it is like a fresh restart and it runs setup again.

Here are the salient features of the code:

  • After wake-up the code sets switchPin (default GPIO15) to high. This turns on the transistor, which in turn switches on the HC-SR04-P sensor. Before going to deep sleep it sets the pin back to low, turning off the transistor and the HC-SR04-P, making sure it doesn't consume any more precious battery power.
  • If the modePIN (default GPIO14) is low the code goes in OTA mode instead of measurement mode. With OTA (over-the-air update) we can update the firmware over Wifi instead of the serial port. In our case this is quite convenient since we don't have to connect the serial to USB adapter anymore for further updates. Just set GPIO14 to low (with the OTA switch in the electronic circuit), reset the ESP8266 (with the reset switch) and it should become available in the Arduino IDE for upload.
  • On the analog PIN (A0), we measure the voltage of the battery. This allows us to turn off our device, aka permanent deep-sleep, if the voltage gets too low, below minVoltage, to protect the batteries from over-discharge. The analog measurement is not very accurate, we do numMeasuresBattery (default 10) measures and take the average to improve accuracy.
  • The distance measurement of the HC-SR04-P sensor is done in the function distanceMeasurement. To improve accuracy the measurement is repeated numMeasuresDistance (default 3) times.
  • There is a funtion to calculate the speedOfSound from the temperature, humidity and pressure measure by the BME280 sensor. The default I2C address of the BME280 is 0x76, but if it doesn't work you might need to change it to 0x77: bool bme280Started=bme280.begin(0x77);
  • We'll use the BME280 in forced mode, which means it takes one measurement and goes back to sleep to save power.
  • If you set capacity (l), fullDistance (cm) and area (m2), the code calculates the remaining volume of the water tank from the distance measurement: double remainingVolume=capacity+10.0*(fullDistance-distance)*area; and upload this to ThingSpeak. If you keep the default values it uploads the distance to the water surface in cm.

Step 6: Building the Electronic Circuit

Above is the diagram of the electronic circuit. It's quite large for one breadboard, especially with the oversized adapter plate and the trick with the U-shaped wires. At some point I certainly wished I had used the alternative of connecting two breadboards, but in the end I managed.

Here are the important features of the circuit:

  • There are two voltages that play a role: the input voltage from the battery (around 3.75V) and the 3.3V that feeds the ESP8266 and the BME280. I put the 3.3V on the left rail of the breakboard and the 3.75V on the right rail. The voltage regulator converts the 3.75V to 3.3V. Following the instructions in the datasheet I added 1 μF capacitors to the input and output of the voltage regulator to increase stability.
  • GPIO15 of the ESP8266 is connected to the gate of the transistor. This allows the ESP8266 to turn on the transistor and thus the ultrasonic sensor when active and turn it off when going in deep sleep.
  • GPIO14 is connected to a switch, the OTA switch. Closing the switch gives the signal to the ESP8266 we want to start in OTA mode next, i.e. after we press (close and open) the RESET switch, and upload a new sketch over-the-air.
  • The RST and GPIO2 pins are connected as in the programming diagram. The RST pin is now also connected to GPIO16 to allow the ESP8266 to wake up from deep sleep.
  • The pins TRIG and ECHO of the ultrasonic sensor are connected to GPIO12 and GPIO13, while the pins SCL and SDA of the BME280 are connected to GPIO5 and GPIO4.
  • Finally, the analog pin ADC is via a voltage divider connected to the input voltage. This allows to measure the input voltage to check the charge of the batteries. The ADC pin can measure voltages between 0V and 1V. For the voltage divider we chose resistors of 100K and 470K. This means that the voltage at the ADC pin is given by: V_ADC = 100K/(100K+470K) V_in. Taking V_ADC=1V this means we can measure input voltages up to V_in=570/100 V_ADC = 5.7V. As for power consumption there is also some current leaking through the voltage divider. With V_in=3.75V from the batteries we find I_leak = 3.75V/570K=6.6 μA.

Even when the circuit is running from batteries, it is possible to connect the USB to serial adapter. Just make sure to unplug VCC of the adapter and connect GND, RX and TX as in the programming diagram. This makes it possible to open the Serial Monitor in the Arduino IDE to read the debugging messages and make sure everything is working as expected.

For the complete circuit I measured a current consumption of 50 μA in deep sleep when running from batteries. This include the ESP8266, the BME280, the ultrasonic sensor (turned off by the transistor) and leakage through the voltage divider and perhaps other leakages. So that is not too bad!

I found that the total active time is about 7 seconds, of which 4.25 seconds to connect to the Wifi and 1.25 seconds to send the data to ThingSpeak. So with a active current of 80mA I found 160 μAh per hour for the active time. Adding 50 μAh per hour for the deep-sleep state we have in total 210 μAh per hour. This means that 2600 mAh batteries theoretically last 12400 hours=515 days. This is the absolute maximum if we could use the full capacity of the batteries (which is not the case) and there are no leakages that I didn't find with my current measurements. So I have yet to see whether this really pans out.

Step 7: Finishing the Sensor

I put the sensor in a plastic 1 litre container, which used to contain soup. At the bottom I made two holes to fit the "eyes" of the HC-SR04-P sensor. Apart from the holes the container should be waterproof. It is then attached to the wall of the water tank with a circular ring that is normally used for a rainwater drain pipe.

Have fun with the project!

Battery Powered Contest

Participated in the
Battery Powered Contest