Solar-powered IoT Ultrasonic Oil Tank Monitor by Steve M. Potter




About: AI consultant and semi-retired brain scientist and former professor at Georgia Tech.

Do you have a tank whose fluid level you want to keep track of? Even if not, you may have an idea for an IoT project, or a solar-powered project, or a Particle Photon project. This Instructable includes many useful details and photos that will teach you (among other things):

  • How to choose and use ultrasonic rangefinders
  • How to measure small bidirectional DC currents with an Arduino (battery charging and discharging)
  • How to log and share sensor data in the cloud
  • How to charge your outdoor IoT project with a solar panel
  • How to deal with varying or unmatched voltages
  • Debugging. Not bugs in software, but actual bugs!

I describe here a system that will send your phone and computers a Pushbullet warning if the tank needs filling. It includes data logging in the ThingSpeak cloud to monitor and share usage and other data. We have a 1000-litre kerosene tank outside our house that supplies heating oil to our boiler, which in turn distributes the hot water to radiators around the house. Unfortunately, these oil tanks are opaque and there is no easy way to know how much kerosene is left until the boiler stops working on a cold day :( Traditionally, people here in Ireland periodically poke a pole down the filling hole, an improvised dip-stick, to see how much oil is left. A fancy tank may have a sight-glass, but that still requires you to brave the cold to check it regularly. With my Oilwatcher, you can check oil levels with any phone or computer. My Instructable project was inspired by the similar one by Ventz (who was very helpful), but because I don't have power near my tank, I had to figure out how to make mine solar powered. I also completely re-wrote the code to log various diagnostic and weather data. I have been using this setup since February 2018, and for the past few months, it has been very reliable and useful. I will describe how I built it, some of the problems I had, and the solutions I came up with. The cost to build your own would be between $100 and $200. Recently, commercial solutions have been developed, e.g. the Apollo Smart oil monitor in Europe or the Smart Oil Gauge in the US, but if you want the fun and flexibility of a homemade hackable smart IoT system with automated alerts, data logging and sharing, read on! (NOTE: Many of my photos have captions. Click on an image to start the slideshow and look for rectangle overlays. When you mouse over them, the caption appears.) This Instructable is entered in the Electronics Tips & Tricks Contest...Please vote for it if you like it!

Step 1: Parts, Supplies and Tools Needed


Tools and supplies:

  • Soldering station
  • wire strippers
  • Multimeter
  • Benchtop power supply
  • Heat shrink tubing & hot air wand
  • Hot-glue gun
  • Hand drill or Dremel tool
  • Prototyping Photon Shield PCB (Free from Bluz at the Maker Faire. Here is a nice option from Adafruit.)
  • PVC pipe and saw to cut it to the height of your tank
  • Polypropylene plastic food storage box to contain electronics
  • Hookup wire
  • Metal brackets
  • Straps to hold down system but allow removal for debugging
  • Solar panel stand (scavenged computer monitor stand)
  • Clear 5-minute epoxy
  • PC-7 Epoxy paste - very strong and thick!
  • Nitrile gloves
  • Silicone rubber glue
  • optional: Oscilloscope for debugging sensor communications

Step 2: Ultrasonic Rangefinders - US-100 Wins!

The theory of operation for this approach to tank level monitoring is pretty straightforward: bounce sound pulses off the fluid surface, and by measuring the echo's delay, calculate how far down in the tank the level is. In practice, it was not at all straightforward. Thankfully, there are lots of cheap ultrasonic rangefinders used for hobby robots that will do an OK job. I originally used the HC-SR04. This device outputs a TTL pulse whose length corresponds to the distance, and the Arduino (or in my case, Photon) has to measure its length accurately. This proved to be very tricky and fraught with noise problems. It had poor accuracy (repeatability results vary by ~2cm), poor precision (best possible is 1 cm using NewPing library), and poor reliability (some data were completely wrong).

Thanks to Andreas Spiess, I discovered the US-100 ultrasonic rangefinder, which can be found for less than €3 and has a digital mode that responds with the distance to the target in mm (not cm, as with other digital ultrasonic rangefinders). It does the conversion from echo delay to distance internally, not in the Arduino, unlike most analog ultrasonic rangefinders. I found that the HC-SR04 was susceptible to noise on the line between it and the Photon. A digital signal is less prone to noise problems. The US-100 also includes a temperature sensor it uses to correct for changes in the speed of sound with temperature. Another benefit is that it can be used at 3.3V, unlike most ultrasonic rangefinders which need 5V to work well. To allow it to survive the elements of an all-weather application like this, I soldered the connecting wires and encased the circuit board in 5-minute epoxy. Pictured is my tank's vent cap with the sensor held in place with hot glue. Kerosene (heating oil) is not nearly as volatile or easy to ignite as gasoline, so there is little or no danger of explosions from this type of device. (Obviously, if you are measuring a more flammable liquid, you would need to carefully isolate any electronics from the vapour)

Banana Robotics sells the US-100 for $5 and has the best description I could find for how to use it in its digital (Serial Data) and analog (Pulse Width) modes, which you select with a jumper. If you scan my code at the end of this Instructable, you will see you just need to send the US-100 a command (0x55 or 0x50) via the Photon's TX line to query the sensor and it replies in a few ms via the RX line with either the distance in mm or the temperature in degrees celsius.

Step 3: False Echo Problems -- Bugs Fixed!

As you can see in the photo, my tank has a lot of internal structure to its shape that caused me lots of problems with false echoes that were not coming from the oil surface. Oil consumption would seem to cease at some point, even when the boiler was working away burning up oil. The false echo problems don't get revealed until the oil level goes down, exposing internal tank structures. I had some success focusing the ultrasonic beam with 10cm of heat shrink tubing applied to the sending and receiving transducers (see photo). But this approach was very sensitive to exactly how I put the cap back onto the tank. Better than the small heat shrink tubes, I came up with an idea that, it turns out, has been invented long ago for this purpose, called a "stilling tube". Mine is a PVC pipe that extends from the very bottom of the tank to the top and the ultrasonic transponder is placed inside its top end. At its bottom end are a couple notches to allow oil to easily pour into and out of the pipe from below so the level inside the pipe is always the same as the rest of the tank. It is called a stilling tube because the level inside will be relatively flat and calm even during filling. (That was important during calibration.) And because the inside surface of the pipe is quite smooth, there are no false echoes to worry about. Except for the earwigs! I was getting odd readings at random, rarely. When I took the cap off to investigate, I saw about 5 squirmy bugs run away, and one headed down the PVC pipe! I thought it was hilarious that the errors in my data were not due to a bug in my code, but an actual bug in my tank! Now I have closed up the vent cap a bit more securely with a nitrile glove and some silicone rubber to "debug" it and discourage earwigs and spiders from setting up shop in there. It is not a perfect seal, to allow for expansion and contraction of the air in the tank.

Step 4: The Particle Photon Is and Is NOT an Arduino

This IoT project requires a microprocessor with Wifi. I chose the Particle Photon because that is what Ventz used for his tank monitor.

If I were starting this project now, I would probably use one of the WiFi-enabled Arduinos available from, such as the Arduino MKR WiFi 1010 . I am not unhappy with the Particle Photon, but they have basically created their own parallel Arduino-like universe instead of just making their products usable with the Arduino IDE. Their Web IDE is very much like the Arduino IDE, but each library you might want to use has to be imported and verified by them to be able to easily add it to your sketch. Something as simple as reprogramming the Photon via USB or even using a serial monitor for debugging is difficult because it requires layers upon layers of software to be installed (Homebrew, dfu-util, NPM, Xcode, etc etc) and to learn a command line interface (CLI). They expect you to reprogram it over the air (OTA) but that is not always possible because the device is offline and sleeping most of the time. It was difficult to install Particle's CLI due to MacOS X High Sierra's overzealous security changes. Although Particle's online documentation is generally pretty good, navigating it and finding things is unduly complicated. I usually just give up and use a Google search instead. On the plus side, Particle's customer service is excellent, and questions get detailed replies from tech support in a day or less. Another option would be to use an ESP32, which now seems to have pretty good functionality in the Arduino IDE, and less overall power consumption.

The Photon is a 3.3V device powered by microUSB (5V) or 3.6-5.5V on its Vin pin. Its digital inputs are all 5V tolerant ("FT") but its Analog pins are NOT. Be sure NOT to apply more than 3.3V to them.

Step 5: Battery: Sealed Lead-acid Gel Cell

I first tried using a LiPo battery, but soon found out that it was pretty worthless when the temperature fell below freezing. This is why electric cars need battery heaters to get a reasonable range in the winter. As I know from starting old-fashioned gasoline/petrol cars on cold winter mornings, lead-acid batteries work well in the cold. A LiPo can be quickly ruined by charging it in freezing conditions.

The electronics I used draw about 80 mA when active, probably a lot of that being used for the WiFi connection.

So in theory, this battery should be able to power the system for a couple days with no sun. Since there may well be more than a couple overcast days here in Ireland, I usually put my Photon to sleep for 5-10 min at the end of each loop, like so:

System.sleep(WAKEPIN, RISING, SleepSecs);

and the WAKEPIN is connected to a momentary switch (to pull up the line from 0 to 3.3V) that I can use to help re-program the Photon without opening the box, if it is sleeping. I also added a delay of about 10 seconds in the loop, so that if I time it right, I can re-flash the Photon OTA (over the air) via WiFi before it goes to sleep.

These lead-acid batteries are "sealed" but have a vent that can emit flammable hydrogen gas when charged rapidly so it is probably best to make sure there is some sort of rainproof vent on your enclosure.

Step 6: Oilwatcher Schematic Diagram

Here I have sketched the wiring diagram for my entire project. It went through many versions and upgrades, all scattered through my maker notebooks. So I took it down, opened the box, and retraced everything as it is right now. It is not perfect, but it works!... See the Calibration step for info about setting the trim pots.

I welcome your suggestions and ideas for improvement. If you make a version for yourself, please tell us about it in the comments or better yet, write your own Instructable!

Step 7: Solar Power and Mounting

I soldered two solar panels in series to charge the "6V" lead acid battery, through a low-dropout adjustable voltage regulator (MIC29302). The panels are rated as 5V 500mA 2.5W, and are 13 x 15cm each. They have absolutely no circuitry in them, just solar cells, so I set the adjustable voltage regulator to 7.3V, which the battery says on its front is a good charging voltage. I found out that solar panels when dimly lit become a great way to discharge batteries. I had invented a Solar Panel Defrosting System! So I included a diode in series to make sure current only flows from them into the battery, not out. Make sure it is a beefy diode, as charging currents may be up to 1A. Be sure to weatherproof all connections with epoxy or silicone rubber to avoid corrosion. And unlike my rat's nest, insulate all exposed PCB leads, solder, wires, etc. to prevent letting out the magic smoke that keeps it all working. For my Outside DS18B20 temperature sensor, I sprung for the more expensive water-tight version.

To power the Photon, I connected the battery to the Verter Buck/boost converter from Adafruit . This can take a wide range of DC voltages (3-12VDC) and turn them into the 5VDC that the Photon takes via its micro USB port. Thus, my Oilwatcher keeps working in the dark until the lead-acid battery is really dead. Unlike LiPo batteries, Lead-acid gel-cell batteries are not ruined by completely discharging them.

I mounted the solar panels with a couple metal brackets and some strong weatherproof epoxy paste (PC-7) to an old computer monitor stand. I have several of those stands from my Artificial Window Instructable project. You should locate and point your solar panels carefully, to avoid shadows and collect the most sunshine, especially on shorter winter days. For that reason, I aim them at the height of the noon sun on a winter day, and have one panel angled to catch more morning sun and one to catch more afternoon sun. By monitoring charging current you can optimize this. There are apps and websites that can also help with this, based on your latitude. You should mount the panels solidly to withstand wind and other outside hazards. Mine is on a 4x4 wood post that used to hold up a fence around our tank.

Step 8: Calibration: Volumes and Voltages

Tank mm to L calibration

To make a useful device for measuring how many litres (or gallons) you have left in your tank, you need to convert the rangefinder's signal, in mm (or cm), to litres. If the tank were a vertical cylinder or a perfect rectangular prism, this would be a simple matter of measuring the tank's dimensions and doing a little math. But as you see, my tank is a bulging oval shape, with two voids in the middle, and various ridges and whatnot. This suggested to me that 1 cm of echo distance change would represent a varying amount of litres at different levels. To measure directly how the volume changes as a function of oil level, I wrote a sketch to collect data as rapidly as possible during tank refilling (given the limitations of the Particle Photon's cloud service, about every 2 seconds). I assume that the truck that fills the tank has a pump that pumps at a constant flow rate, which the driver sets with a big lever. And the truck is carefully metering the volume delivered (1000 L total). I was surprised to see in the attached graph of the transponder data collected during filling, that the curve is quite linear across the entire range! Perhaps the tank designers adjusted the voids in the centre of the tank to compensate for the increased width at its centre. I converted the time axis to litres, and measured a conversion factor using a linear fit to the curve: 1.10 L/mm. I also took note of the transponder reading when the tank is empty, meaning when the spigot sucks air and the boiler quits. This turns out to be 1304 mm for my tank. This is important for setting a useful alarm; you need to know when the boiler quits, not when the tank is dry. (The spigot is not low enough to drain the tank dry.)

Voltage and current calibration

The other thing I wanted to calibrate, to predict when the battery would die if there were too many cloudy days in a row, is the electrical current drawn by the whole system, and the battery voltage. To do this, I made a crude "high-side" bidirectional current sensor using a 1-ohm shunt resistor in series with the (+) power from the battery. I measure the voltages on each side ("tap") of this resistor relative to ground. The difference between the two measured voltages, or voltage drop across that resistor, is proportional to the current through it (Ohm's Law, I=V/R). With R=1 ohm, I=V, so 80mV is 80mA. I divide each tapped voltage by a known amount (~factor of 7/3 to bring 7.2V down to ~3.3V) with voltage dividers (10k trim pots, see schematic diagram later in this Instructable) to prevent exceeding the 3.3V max of the Photon's analog inputs. So I used two of the Photon's analog input lines to measure this voltage drop (and another to measure Vbatt). To calibrate this setup, I powered the whole device from a digital benchtop power supply with a good current readout, and got current measurements under various conditions: WiFi connected and Photon active (~90 mA) and sleeping (~1-4mA). Because the voltage drop across a 1-ohm resistor, especially after the voltage dividers, is really tiny (and the photon's A/D lines are only 12 bits= 4096 levels), I needed to average 100 measurements to get a reasonably noise-free current value. These can be collected quite rapidly. Ideally, I should have made a differential amplifier to boost that current signal, but this simple averaging approach works pretty well for measuring bidirectional current flow (charging and discharging). I tried using a 10-ohm shunt to increase the signal, but the Photon couldn't get enough juice through it to work. It got stuck in brown-out mode. I could have used trim pots of less than 10k for better noise immunity but at the expense of wasting the battery heating the trim pots. 1k would be 7mA wasted per trim pot.

So with the analog lines giving me digital units from 0-4095, and with current and voltage measurements from the benchtop power supply, I was able to put conversion factors into my sketch (see my code in the last step). Obviously, you would need to calibrate your setup yourself to get good data.

I realize that I don't really need to measure Vbatt with a separate analog input line, and its own voltage divider, but I kept that because it is conceptually clearer and because the current monitor taps have to be located symmetrically right next to the 1 ohm resistor. (I already had the voltage monitor in there on a separate board before I added the current measuring system.)

Step 9: Using Pushbullet to Send Alarms

Pushbullet is a cross-platform alerting and messaging service. Your Photon's sketch will include some "alert" messages that will be "published" to Particle's cloud, e.g., when the oil level gets below some threshold. Install the Pushbullet app on your phone and it will echo any alerts, notifications, and messages you may wish to all your other devices. Thus, once you have it set up properly, it will be hard to miss the important "Oil is low!" alert that your Oilwatcher device will initiate.

Sign up for an account at and read the documentation on their API (application programming interface). You can also read key background on Particle's Console - Integrations section:

When you start to get overwhelmed, just read Ventz Petkov's blog for a step-by-step on how to set things up.

He also includes a description of how to get started with the Particle Photon. And lots of screenshots of the process are in his Instructable. He mentions that he moved to Pushover instead of Pushbullet, so there are some options for exactly how to implement getting alerts on your phone.

Step 10: Using ThingSpeak to Log and Share Your Data

If you just want to live in the moment and forget about your heating oil until it runs out, you can skip this section.

For those like me who are interested in all the scientific and geeky details of how much oil you use at different times of year, or how much sunshine or cold happened, ThingSpeak is for you! With the solar panels as a sun sensor, and the temperature sensors I included in my project, I am slowly building a proper weather station! Thingspeak is a place to collect and analyze data of any type, using lots of graphs and if you wish, complicated math. It was created by The MathWorks, who also created Matlab, a favourite among scientists like me for dealing with Big Data. "The open IoT platform with MATLAB analytics" is their tagline. You can keep it all private, or share your data with the whole world, as I have done here. You can have your Photon sketch write up to 8 different variables to "Fields" in your Channel at their cloud every few seconds. After you have created an account at ThingSpeak, create a Channel as a place to collect and work with your data. Note its channel number. Set up the Channel with Field names and note their Field numbers. Then you can get an API Key from the API Keys tab. Use this in your Oilwatcher sketch setup:

unsigned long ThingSpeakChannelNumber = 123456;
const char *  ThingSpeakWriteAPIKey = "THIS1ISAFAKEKEY45678";
In the main loop, each time a variable of interest is updated to a new value, call
ThingSpeak.setField(5, US100tempData); // Field 5 is for the US-100's onboard temperature sensor
ThingSpeak.setField(6, litres_of_oil);  // Field 6 is for my Litres Remaining data
and near the end of each loop of your sketch, you send all the fields of data in one go:
ThingSpeak.writeFields(ThingSpeakChannelNumber, ThingSpeakWriteAPIKey);
Then in your Channel's Private and/or Public view tab, create visualizations (charts) of your data and they will be updated automatically each time a new set of data is broadcasted by your sketch.

Step 11: The Oilwatcher Sketch

Attached is the .ino file that is my Oilwatcher code AKA the Photon's firmware. The .zip file also includes the library called RunningMedian by Rob Tillaart, not yet on Particle's web IDE. I got the latest version of that library here:

The other libraries included at the top are in Particle's libraries, but don't forget to actually #include them in your project to get the code to compile properly.

If you do decide to use all the bells and whistles I included, like the current monitoring, ThingSpeak etc., there will be some constants in the header of the sketch you must personalize. And of course, the oil level at which the code sends out an alarm must be carefully set for your tank.

Happy hacking!

Please add your comments and questions below...

Here is my Oilwatcher sketch (also attached to this step):

OilWatcherus100 sketch by Steve M. Potter   steve.potter at
To monitor heating oil tank level remotely, and send Pushbullet alerts when fuel is getting low.
Uses the Particle Photon board, Adafruit's Verter voltage buck-boost converter, an additional adjustable voltage regulator, and PushBullet
Verter is available in EU from Pimoroni
Uses US-100 ultrasonic rangefinder.

Inspired by code and design by Ventz:
and also from his Instructable:

Last update 2018-10-03
Code is kept on  called "OilWatcherUS100"

// Libraries to include:
#include "RunningMedian.h"  // by  Rob.Tillaart at
#include <ThingSpeak.h>  // for data logging and analysis, e.g. see
#include <OneWire.h>   //  For the DS18B20 digital temperature sensors
#include <DS18B20.h> // For the DS18B20 digital temperature sensors
//#include <NewPing.h> // Improved communication to the HC-SR04 ultrasonic rangefinder, by Tim Eckel
                       // Only for use in analog pulse mode.

#define VBATT        A1  // blue wire. Monitors the voltage of the battery, through a 1/10 voltage divider.
#define POSCURPIN    A4  // white Wire attached to one side of a 1-ohm shunt resistor in series with the battery, then a voltage divider.
#define NEGCURPIN    A5  // blue Wire attached to the other side of a 1-ohm shunt resistor in series with the battery, then a voltage divider.
#define TEMPPIN      D5  // yellow Onewire data line for temperature probe.
#define MAX_DISTANCE 1400 // Maximum distance we want to ping for (in mm). 
#define ONBOARDLED   D7
#define WAKEPIN      A2 // white wire, to wake from sleep with button.
#define SENSORS      2 // number of DS18B20 temperature sensors

// Variables:
TCPClient client;  // for ThingSpeak

byte   mac[6]; // the MAC address of the Photon
int    battThreshold = 30; // Percent (from Dead to Full) below which a LOW BATTERY! warning is triggered (should be > 2.5V).
double FullChargeVolts = 6.5;  // set for battery being used.
double DeadBattVolts = 5.5;  // Lead Acid batt voltage that the system fails.
float  voltage = 0.0; // Variable to keep track of battery voltage
double BattVolts = 0.0;
double battCalib = 2.16; //correction factor. TO RECALIBRATE THIS, measure batt at its terminals with o-scope.
double soc = 0.0; // Variable to keep track of battery's state-of-charge (SOC)
bool   battalert = LOW; // Variable to keep track of whether battery alert has been triggered
double currentConversion = 2.27;  //To convert A/D counts to mA of current in and out of the battery.
float  BattCurrent = 0.0;
int    negCurCounts; 
int    posCurCounts;
int    negCurOffset = 0;
int    posCurOffset = -27; // to calibrate the current-measuring lines, jumper the 1ohm resistor and make current zero.
int    CurCountsDiff = 0;

int    cm = 0;  // Distance between sensor and oil surface. NewPing returns it in whole cm.
bool   LowOilAlert = LOW; // Goes high when oil level gets below threshold (50mm presently)
int    cm_of_oil = 200; // This is zero when it gets down to the spigot, not the bottom of the tank.
int    mm_of_oil = 2000; //This is zero when it gets down to the spigot, not the bottom of the tank.
int    litres_of_oil = 0;
int    ZeroOilmm = 1304; // When my tank begins to suck air, the mm reading is this.

int    difference = 0; // variable to hold temporary info during calculations
int    q; // for loop counters.
int    SleepSecs = 60; // seconds for device to sleep each loop.
unsigned long lastUpdate = 0;

float  BoxCelsius = NAN;
float  OutsideCelsius = NAN;
String BoxTempStr;
String OutsideTempStr;
DS18B20  TempSensors(TEMPPIN); //Sets name for calls to DS18B20 library, Pin D5 for 1-wire Temp Sensors.
retained uint8_t sensorAddresses[SENSORS][8];  // DS18B20 addresses retained across deep sleep in backup RAM as long as power is on VBAT or VIN of Photon
unsigned long ThingSpeakChannelNumber = ; //Enter your Channel number
const char *  ThingSpeakWriteAPIKey = "";  // Enter your API key
  // US-100 ultrasonic rangefinder:
 unsigned int  MSByteDistance = 0;
 unsigned int  LSByteDistance = 0;
 unsigned int  mmDistance = 0;
 int           Median_mm = 0;                // Some of these I tried to use unsigned int and got an "ambiguous" compile error.
 int           TempmmDistance = 0;
 int           US100tempData = 0;
 int           junk;
 unsigned long beginmillis = 0;
 unsigned long nowmillis = 0;
 unsigned long elapsedmillis = 0;
 RunningMedian US100reading = RunningMedian(9);
 // SETUP ______________________________________________________________________________________________

void setup() {
  pinMode(VBATT, INPUT);
  Particle.variable("Box °C", BoxTempStr);  // temp sensor in box.
  Particle.variable("mm_of_oil",   mm_of_oil); // mm of oil remaining in tank (to spigot).
  Particle.variable("L of Oil",   litres_of_oil); // litres of oil remaining in tank (to spigot).
  Particle.variable("mmDistance",   mm_of_oil); // Average mm measured by rangefinder.
  Particle.variable("BattVolts",   BattVolts); // battery voltage.
  Particle.variable("BattPercent", soc);  // Li-ion battery State of Charge
  Particle.variable("Tank °C", US100tempData);  // Thermometer in ultrasonic sensor
  Particle.variable("Median_mm", Median_mm);  // Median mm measured by rangefinder.
  Particle.variable("Outside °C", OutsideTempStr);  // DS18B20 temperature outside.
  Particle.variable("Raw mm data", TempmmDistance);
  	// To read the values from a browser, go to:

  STARTUP(WiFi.selectAntenna(ANT_EXTERNAL)); // selects the external antenna connector, not chip antenna.
  Serial.begin(115200); // Open serial monitor at 115200 baud to see ping results.
  Serial.println("OilWatcher by Steve M. Potter. Last update 2018-10-03");
  Serial1.begin(9600, SERIAL_8N1); // for comm to/from the US-100 via RX and TX pins.

  Serial.print("MAC address of Photon: ");
  Serial.print(mac[0]&0x0f,HEX);  // This may be in reverse order. 
  TempSensors.resetsearch();                 // initialise for DS18B20 temperature sensor search
  for (int i = 0; i < SENSORS; i++) 
   {[i]); //  try to read the 1-wire sensor addresses and if available, store them.
} // end setup()

// MAIN LOOP _________________________________________________________________________________________________

void loop() {
  mmDistance  = 0;
  int DataToAvg = 9;
  for (int avgloop = 1; avgloop < (DataToAvg + 1); avgloop++)
   Serial1.flush(); // Clear the serial1 buffer. 
   Serial1.write(0x55); // Send a "distance measure" command to US-100
   delay(200); // US100 response time depends on distance.
   if (Serial1.available() >= 2)  // at least 2 bytes are in buffer
        MSByteDistance =; // Read both bytes
        LSByteDistance  =;//
        TempmmDistance = (MSByteDistance * 256 + LSByteDistance);
        Particle.publish("Raw mm data", String(TempmmDistance), 60, PRIVATE);
        mmDistance  = mmDistance + TempmmDistance; // calculate distance in mm. Add to running sum.
        if ((TempmmDistance > 50) && (TempmmDistance < MAX_DISTANCE))   // Test that the distance is in range.
          US100reading.add(TempmmDistance); // put another datum into the buffer for a running median.
         } else    avgloop--; // discard this datum.
  mmDistance = mmDistance/DataToAvg; // calculate the mean of N measurements.
  Serial.print("Raw mm data: ");
  Median_mm = US100reading.getMedian(); // get the current running median value of mm from sensor to surface of oil.
  Particle.publish("Median_mm", String(Median_mm), 60, PRIVATE);
  Serial.print("   Median mm data: ");
  mm_of_oil = ZeroOilmm - Median_mm; // I determined that my boiler quits when mm = 1304.
  Particle.publish("mm_of_oil", String(mm_of_oil), 60, PRIVATE);
  delay(1000); // wait for last publish to finish.
  if ((mm_of_oil > 0) && (mm_of_oil < MAX_DISTANCE))
   ThingSpeak.setField(1, mm_of_oil);
   ThingSpeak.setField(7, TempmmDistance);
   delay(1000); // wait for last Publish to finish.
   // Calculate litres....Our tank is 1.10 mm/L 
   litres_of_oil = 0.909 * mm_of_oil;  // Calibrated 2018-02-24 during tank filling.
   Particle.publish("Oil level (L)", String(litres_of_oil), 60, PRIVATE);
   ThingSpeak.setField(6, litres_of_oil);

// Monitor the battery voltage
  voltage = 0;
  for (int z = 1; z < 101; z++)  // It is pretty noisy so we take an average of 100 values.
   voltage += battCalib * ((analogRead(VBATT) / 4096.0) * 3.3); // battery monitor is via a voltage divider.
  voltage = (voltage/100);
  BattVolts = voltage;
  soc = ((voltage - DeadBattVolts) / (FullChargeVolts - DeadBattVolts)) * 100.0;
  Particle.publish("BattVolts",   String(BattVolts, 2), 60, PRIVATE);
  delay(1000); // Can only publish 4 variables per second. 
  Particle.publish("BattPercent", String(soc, 1), 60, PRIVATE);
// Monitor the charge/discharge current. Negative values mean the solar panel is effectively charging the battery.
  CurCountsDiff = 0;
  for (q = 1; q < 101; q++)   // The readings are so noisy, need to average 100 of them.
   negCurCounts = analogRead(NEGCURPIN);
   posCurCounts = (analogRead(POSCURPIN) + posCurOffset);
   difference = (posCurCounts - negCurCounts);
   CurCountsDiff = CurCountsDiff + difference;
  CurCountsDiff = (CurCountsDiff/100);
  BattCurrent = (currentConversion * (CurCountsDiff)); 
  Particle.publish("BattCurrent mA", String(BattCurrent, 0), 60, PRIVATE);
  ThingSpeak.setField(4, BattCurrent);
 // Send Alerts via Pushbullet:
  if ((mm_of_oil < 50) && (mm_of_oil > 45))  // Only want Pushbullet to send a few warnings
         Particle.publish("Alert", "Heating Oil Level is 5cm - Refill SOON!", PRIVATE); 
         LowOilAlert = HIGH; 
  if (soc < battThreshold) 
	   Particle.publish("Alert", "Oilwatcher battery is very low. Recharge it.", PRIVATE); 
	   battalert = HIGH;

// Read temperature from the US-100 ultrasonic rangefinder's temp sensor at the top of the tank. The tank air heats up in the sun.
	 while (Serial1.available() >= 1)  // seemed like flush() was not working so I added this.
	     junk =;
    Serial1.write(0x50); // send command to request temperature byte.
    delay(50); // temp response takes about 2ms after command ends.
    if (Serial1.available() >= 1) 
        US100tempData =; 
        if ((US100tempData > 1) && (US100tempData < 130)) 
            US100tempData -= 45; // Correct by the 45 degree offset of the US100. 
            Particle.publish("Tank °C", String(US100tempData), 60, PRIVATE);
            ThingSpeak.setField(5, US100tempData);

// Read temperature from the DS18B20 sensors in electronics box and outside.
  BoxCelsius = TempSensors.getTemperature(sensorAddresses[1]);  // Will it always find the 1-wire sensor addresses in the same order?
  BoxTempStr = String(BoxCelsius, 1); 
  Particle.publish("Box °C", BoxTempStr, PRIVATE); 
  ThingSpeak.setField(3, BoxTempStr);
  OutsideCelsius = TempSensors.getTemperature(sensorAddresses[0]);
  OutsideTempStr = String(OutsideCelsius, 1);
  Particle.publish("Outside °C", OutsideTempStr, PRIVATE); 
  ThingSpeak.setField(8, OutsideTempStr);
//  Send all data to ThingSpeak for graphing and calculations.
  ThingSpeak.writeFields(ThingSpeakChannelNumber, ThingSpeakWriteAPIKey); 
                 // give time for temperature to publish before going to sleep.
  delay(10000);  // In case I need to reprogram it, here is a chance when it is not sleeping.
  System.sleep(WAKEPIN, RISING, SleepSecs);  // Turn off WiFi and pause execution, preserving variables. Wakes after set # of seconds
                                      // A momentary button on the box allows waking for easier re-programming.
                                      // Hit Flash very soon after hitting the wakeup button. Or just before.
  //System.reset();  // This was one way to allow it to not lose its ability connect to wifi when the router's IP address changed during sleep.
                   // Disadvantage is it only loops once thru the code. No variables remembered.

}  // end void loop()



    • Toys Contest

      Toys Contest
    • First Time Author

      First Time Author
    • PCB Contest

      PCB Contest

    9 Discussions


    7 weeks ago

    Nice project and well done instructable. However, it would be suitable to measure levels of some non-flammable liquids, while for kerosene it poses potential hazard of ignition. The circuit which is exposed to tank inner atmosphere can deliver enough energy to ignite vapours that will build up in the tank. Unless the energy to the US sensor is limited by a barrier or the device is sealed to prevent contact with explosive gases.
    For more on this read about explosion protection -
    Your device should typically be Ex i - intrinsically safe or Ex d - flame proof. Some other types of protection can be found it that wiky.
    Be safe ;)

    4 replies

    Reply 7 weeks ago

    Yes, safety first! I and also Ventz addressed the safety issues in our Instructables. Ultrasonic probes do not generate any sparks (no relays or switches) or have hot components, but if you were to connect or disconnect wires from the sensor you might get a spark, one of the reasons I soldered my cable to the sensor.


    Reply 7 weeks ago

    Definitely wise thing to do to solder wires and to remove connector contacts as a potential source of spark. I would be also afraid of other modes of failure - it is exposed piece of electronics in flammable atmosphere (permanently - zone 0), connected with wires to external circuit (sounds almost like description of a detonator to me). Your CPU board can fail, you can send higher voltage during manipulation with external circuit, wireless can do things, PSU can fail - R&D would typically perform FMEA and address resulting risks. This problem drives whole industry behind Intrinsic safety.


    Reply 7 weeks ago

    I defer to Ventz who addresses this in his Instructable:
    He says: SAFETY INFORMATION: In case anyone wants to know if "this is safe to build/install" -- I have taken this to 2 different Oil companies for feedback/safety considerations, and I have run this by the fire department's Fire Prevention Deputy Chief. Per all 3 - the device is considered completely safe with no risk of fire or explosion. That said, I cannot control your individual environment/what you do with it, so please assume your own risk when installing it. Per the oil companies - electric sparks/open flames will not ignite the oil, so there is no possibility of catching fire/explosion/etc. Nothing compresses/creates a vacuum/creates air pressure to cause an explosion. My favorite quote was "even if you flick lit matches in your oil tank, it will not catch on fire."


    Reply 6 weeks ago

    Thumbs up to Ventz, he went far to make sure his design was safe. Fair message to anyone following this design for hazardous liquids is to check his/her application specifics and consider eventual risks.
    Stevempotter, thanks for great instructable.


    Question 7 weeks ago

    Perhaps I missed it in the post, but what was the diameter of the PVC 'Stilling' tube?

    1 answer

    Answer 7 weeks ago

    Its inside diameter is just big enough to accommodate the transducers of the ultrasonic rangefinder. I did not measure it but I don't think it matters too much, as long as it is bigger than the transducers and fits in the opening of the tank.


    7 weeks ago on Step 5

    Nice. This caught my eye because I have a similar need for an indoor water level pump activator. Just had to leave a comment.