So how does the tank monitor actually work?
The sensor unit sends out an inaudibly high-pitched burst of sound toward the surface of the oil in the tank. When it hears the echo off the surface of the oil, it returns a pulse on the SIG line. That pulse is as long as the time between sending and receiving the echo.
The Parallax Ping))) uses one pin, SIG, for both triggering the ping and receiving the resulting pulse. It also uses 5-volt logic. Therefore, we need a voltage divider to reduce the 5-volt pulse down to the 3.3 volts that the Raspberry Pi can handle. But we don’t want to reduce the 3.3-volt pulse from the Pi that will trigger the Ping))); the Schottky diode allows our trigger pulse to pass reasonably unimpeded while blocking the 5V from the Ping))). The Ping))) will accept a 3.3-volt pulse as a trigger. As an added precaution, a 3.3V Zener diode protects the Pi from overvoltage in case something goes wrong with the resistive voltage divider.
Our sensor code sends the trigger pulse, and then changes the GPIO pin from an output to an input. It then waits for the Ping))) to drive the pin high, while keeping track of the current time. If it doesn’t see the pin go high within 0.02 seconds, the measurement times out—that’s a longer delay than any ping within the Ping)))’s rated distance.
Next, we wait for the Ping))) to drive the GPIO pin back low—the end of the voltage pulse. That time is also noted. Simple subtraction provides the length of the pulse.
This is where the Pi is not ideally suited to the task at hand. Because the Pi is running Linux, a preemptively-scheduled multitasking operating system, the Pi may be busy doing other things instead of running our code when the pin goes high or low. That introduces timing error, which means measurement error. Python isn’t well-suited to reducing this error; it should be possible to write code in C that can leverage the Linux kernel’s real-time scheduling mode, which minimizes the chances of being interrupted by another process. In the meantime, the following things reduce the chances of measurement error:
- Overclocking the Pi. Faster processor speeds mean more frequent checks of the GPIO pin. Also, competing tasks will complete more quickly.
- Avoiding running other processes on the Pi. Using the Pi for anything else—even a web user interface for the oil tank monitor—will increase the chances of resource contention and therefore measurement error.
- Running the oil tank measurement process at the highest user priority. Using “nice -n -20” (which requires superuser privileges or the CAP_SYS_NICE capability) will reduce contention from other userland processes, but won’t help much with kernel interrupts.
With the duration of the Ping)))’s voltage pulse at hand, it’s simple math to determine the distance. The key number is the speed of sound in air. This varies depending on the temperature of the air. At 70°F, it’s 343.963 meters per second. In a 62° basement, it’s 341.355 m/s; at 32°, it’s 331.395 m/s. If the basement and oil tank is very well temperature controlled, one could just plug in a hard-coded value. But oil tanks are usually connected to outdoor air via a vent pipe, and basement temperatures can fluctuate as the furnace cycles on and off. So, I incorporated a temperature sensor to detect the air temperature near the Ping))) sensor. My code uses the approximation
V ? 331.4 + (0.6 × T)
where V is the velocity of sound in air and T is the temperature in degrees Celsius. This approximation is technically for “dry air;” the speed of sound also varies somewhat based on humidity. For now, I’m ignoring the humidity part of the calculation; it’s certainly possible to incorporate a temperature-and-humidity sensor and do more math for an even more precise calculation. On the Pi, that might start to exceed the accuracy of the timing…
Between the inherent limitations of acoustics and the limitations of the Pi, occasionally our sensor result will be way off. Commonly, it will return slightly different answers if you make two measurements right after the other. To minimize this effect, my code takes a number of samples, and then discards the highest third and lowest third of the samples. It averages the remaining samples and rounds the result to the nearest centimeter. (The Ping))) sensor is only rated for 1cm accuracy, so retaining additional “precision” is not useful.) This is a place where I wish I had a better understanding of statistics, to further improve the handling of outliers.
Finally, we subtract a “fudge factor,” which corrects any measurement error. The Ping))) is actually pretty accurate. But there is one measurement error of importance in this design: The distance between the plane of the top of the oil tank and the plane of the Ping))) sensor. It’s unlikely that the sensor will seat so far into the tank that the Ping))) is exactly even with the tank’s top. The fudge factor allows me to compensate for this. Before installing the sensor in the tank, I measured the actual oil level with a stick. I could then easily calculate the fudge factor required to obtain an accurate reading that matches the stick.
The result from the sensor code is the distance from the Ping))) to the oil—the height of the air in the oil tank.
Now we need the dimensions of the inside of the oil tank. There’s a little inaccuracy here, because it’s really difficult to get inside and measure; instead, I measured the outside of the tank and the visible thickness of the tank material, and used that to estimate the inside dimensions. I have tanks that have an oval cross-section with flat sides, vertically oriented (short side of the oval up). To calculate volumes inside such a tank, I need the height of the tank at its widest point, the depth of the tank at the flat part of the sides, and the width of the tank.
Given the height of the tank, I can subtract the height of the air inside the tank (obtained from the sensor) to determine the height of the oil inside the tank.
I found a website that has formulas for calculating volumes inside various types of tanks. For my style of tank, you can calculate the volume by thinking of it as if it were two tanks: A perfectly square tank with a height equal to the distance between the curved parts of the oval end, and a perfectly round tank with a radius equal to the radius of the curved parts of the oval end. (The oval ends are each a semicircle; imagine cutting them off and welding them together to make a full cylinder. The leftover tank bits would make a rectangular solid.)
So, to calculate the full volume of the tank, I calculate the volume of the cylinder and of the rectangular solid and add them up. For my tanks, I find that it comes out a bit less than the nominal capacity of the tank. Somehow, I’m not surprised.
What about the radius of the curve? Well, because the tank doesn’t have a flat spot at the top—it’s a perfect semicircle—I don’t need to measure the radius. I know that the diameter of the imaginary cylinder will be equal to the depth of the tank, because that semicircle has to mate up to the rectangular portion of the side. The radius is half the diameter… or half of the tank’s depth.
As for the volume of oil in the tank, the calculation changes depending on how full the tank is.
- If the oil level is in the rounded bottom portion of the tank, pretend it’s in a cylindrical tank. Imagine you’re looking at the end of the tank. Draw a line across the circle of the tank to represent the oil level. Then use trigonometry to determine the area of the circle beneath that line. Multiply by the length of the tank and you have volume.
- If the oil level is somewhere in the straight-sided middle of the tank, first calculate the volume of a half-full cylinder. Then subtract the radius of that cylinder from the oil height. Now calculate the volume of oil in a rectangular tank that’s filled to that height. That’s a simple HxWxL calculation. Add the two together and you have the volume of oil in the tank.
- If the oil is in the top rounded portion of the tank, it’s easier to calculate the volume of air in the tank and subtract that from the volume of a full tank. As with the first case, this is done by imagining the top part of the tank as a perfect cylinder, and using trigonometry to calculate the area filled with air.
Note that all this math is done using the metric system. The TMP102 sensor returns the temperature in degrees Celsius. That makes it easiest to calculate the speed of sound as meters per second, resulting in a distance measurement in centimeters. Therefore, our volume calculations are in cubic centimeters. I use a simple formula to convert cubic centimeters to U.S. gallons.
My sample code also has a very basic time estimate. All oil furnaces have a rated gallons-per-hour fuel consumption; it’s typically printed on the furnace’s specifications plate. With that value, it’s easy to calculate how many hours of furnace running time are remaining based on the current oil level. I also make a rough guess of how many days worth of heating that runtime equates to, based on an average number of running hours per day. I hard-coded that value based on the last week’s data from my Nest thermostat. Future revisions will monitor daily oil consumption and use that to estimate days remaining. (Unfortunately, Nest does not currently provide furnace runtime in their Works With Nest API, so I can’t get this data directly from the thermostat.)
So what did this project cost me?
The single most expensive part of my build was the Ping))) sensor, at $30. However, I expect one could build a similar unit using the HC-SR04, which costs between $2 and $5 depending on quantity. The HC-SR04 uses separate pins to trigger the pulse and return the result, so using it would require minor changes to the interface circuitry and possibly the code.
The next most expensive part was the Raspberry Pi. I used a Model A+, which has a suggested retail price of $20. However, I found it cheaper to buy on Amazon at $25, because I have Prime shipping. The few retailers with the Model A+ in stock at $20 wanted an average of $6 for shipping.
The plumbing parts cost $14.60. If you’ve got spare PVC parts knocking about, you can economize here. You could also use a simple 2″ cap and glue it in place instead of using the 2″ to 3″ parts and cleanout. You’d have to unscrew the whole thing to clean the sensor, and you’d need to rebuild it if serious repairs were needed but it would cut the cost of pipe in half. I don’t recommend using thinner pipe, because the unit needs to withstand the pressures introduced when the tank is refilled. Care should be taken with alternate materials; some plastics can be dissolved by fuel oil. I chose PVC and acrylic because they both have good resistance to diesel oils.
I used a network adapter that I had on hand. I figure a new adapter off Amazon would cost about $8, for either wired or Wi-Fi.
Not including a case to hold the Pi and the interface board—you can order a suitable project case, or find something to reuse like I did—the approximate cost of the materials to build this project the same way I did is $109. Using an HC-SR04 would probably reduce that to about $81. If you have worked with PVC pipe before and know how to solder, it can be built and installed in a weekend or less.
For comparison, the closest commercial sensor system I’ve found costs $129, and it only provides a 10-segment bar graph. It doesn’t connect to the Internet, and therefore can’t be programmed to send you alerts when your oil is low, adjust your thermostat to conserve oil when the level is critical, provide daily reports on oil levels and consumption, give a close estimate in gallons of remaining fuel or days of oil remaining at current usage, or any of the other things my homebuilt unit can do. The same site offers an Internet-connected controller with an ultrasonic sensor and temperature sensor for $928, which doesn’t appear to offer even as much functionality as my demo program.