Yes, the project is still running. I don’t take time based readings, the values are concatenated to a string every time the level changes, and I read that from an iOS app; I have to initiate that reading, and now that it’s been working so well for a long time, I usually don’t read it more than once a day. The string is reset to an empty string every day at 7 AM. At the top of the pipe, the sensors are only 3/4" apart, so the magnet is large enough to activate 2 at a time if it’s right in the middle between them. The float I’m using, I built out of a piece of 2"x12" redwood. Originally, that’s all it was, but it got so waterlogged that it didn’t float so well any more, so now, I have a piece of foam attached to the bottom of it, so it floats higher. I think you would probably want to use a plastic float instead, if you’re measuring an acid. To answer your last question, yes, it keeps a running list of the water levels until it’s reset at 7 AM.
The code has changed somewhat since I posted this. This is the code I’m running now.
#include <HttpClient.h>
#include "Extras.h"
typedef struct {
float reading;
int timeStamp;
} data;
typedef union{
uint8_t addr;
struct {
uint8_t bit1 : 1; // lsb
uint8_t bit2 : 1;
uint8_t bit4 : 1;
uint8_t bit8 : 1;
uint8_t highNibble : 4;
};
} AddressUnion;
AddressUnion a;
data d;
data allData[100];
int indx;
OneShot timer6AM(6,0); // timer that fires once per day at 6:00
OneShot timer7AM(7,0); // timer that fires once per day at 7:00
OneShot timer7Thirty(7,30); // timer that fires once at 7:30 AM
OneShot timer11_30PM(23,30);
Wait fiveSecs(5, SECONDS); // a hidden millis timer. Can be MILLIS, SECONDS, or MINUTES
Timer fillRateTimer(300000, fillRateTimerHandler);
double fillStartValue;
const int dataPin = D2; // data pin to read the A3212 Hall Effect Switches (connected to the common in/out pin on the CD4067)
const int chipInh0 = A2; // chip inhibit pins for the 3 CD4067 1x16 multiplexers. The chip is selected when the chip inhibit pin is LOW (inhibited when HIGH)
const int chipInh1 = A3;
const int chipInh2 = A4;
const int onesBit = D1; // address pin A on a CD4067
const int twosBit = D0; // address pin B
const int foursBit = A1; // address pin C
const int eightsBit = A0; // address pin D
int counter;
int timeFilled;
double gallonsDown;
double twentyThreeThirtyGallonsDown;
char valuesString[620];
bool shouldReset;
bool shouldMonitorFillRate;
// The first 25 Hall effect switches are 3/4" apart. The next 15 are 1" apart. The last 8 (lowest down in the tank) are 2" apart
// There are two places in the array below where the values are out of order (11&12 and 27&28) -- this is intentional to correct for wiring mistakes
float inchesDown[48] = {0.032, 0.75, 1.5, 2.25, 3, 3.75, 4.5, 5.25, 6, 6.75, 7.5, 9, 8.25, 9.75, 10.5, 11.25, 12, 12.75, 13.5, 14.25, 15, 15.75, 16.5, 17.25, 18, 19, 20, 22, 21, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 35, 37, 39, 41, 43, 45, 47, 49};
http_request_t request;
http_response_t response;
HttpClient http;
http_header_t headers[] = {
{ "Content-Type", "application/json" },
{ NULL, NULL }
};
STARTUP(WiFi.selectAntenna(ANT_EXTERNAL));
SYSTEM_THREAD(ENABLED);
void setup() {
waitFor(Particle.connected, 600000); // wait up to 10 minutes for Wi-Fi router to come back online after power failure
Particle.function("getRSSI", getRSSI);
Particle.function("reset", resetDevice);
Particle.function("fillMonitor", fillMonitor);
Particle.variable("gallonsDown", gallonsDown);
Particle.variable("values", valuesString);
Particle.variable("timeFilled", timeFilled);
pinMode(D7, OUTPUT);
digitalWrite(D7, HIGH);
waitFor(Time.isValid, 10000);
digitalWrite(D7, LOW);
setZone(); // sets time zone to Pacific Daylight Time or Pacific Standard Time depending on the date
request.hostname = "things.ubidots.com";
request.port = 80;
pinMode(dataPin, INPUT_PULLUP);
pinMode(onesBit, OUTPUT);
pinMode(twosBit, OUTPUT);
pinMode(foursBit, OUTPUT);
pinMode(eightsBit, OUTPUT);
pinMode(chipInh0,OUTPUT);
pinMode(chipInh1,OUTPUT);
pinMode(chipInh2,OUTPUT);
fiveSecs.begin(); // nothing here other than start = millis() for a millis-like timer object
//Particle.publish("WellDry", "Well is running dry");
}
void loop() {
if (timer11_30PM.fired()) twentyThreeThirtyGallonsDown = gallonsDown;
if (shouldReset == true) {
shouldReset = false;
System.reset();
}
// Sync time with the cloud once a day at 7:00 AM
if (timer7AM.fired()) {
Particle.syncTime();
waitFor(Time.isValid, 30000);
setZone();
for (int i=0; i<100; i++) {
allData[i] = {0, -1};
}
indx = 0;
valuesString[0] = 0; // reset valueString to empty string once a day
}
if (timer6AM.fired()) {
if (allData[indx].reading > 0.04) {
char sixAMValue[30];
sprintf(sixAMValue,"Tank down by %.1f gallons", allData[indx].reading * 31.33 );
Particle.publish("WaterTankFailure", sixAMValue, 60, PRIVATE); // triggers a webhook that sends a notification to my phone through Pushover
}
}
if (timer7Thirty.fired()) {
timeFilled = 0;
}
if (fiveSecs.isUp()) cycleAddressPins();
}
void cycleAddressPins() {
float total = 0;
counter = 0;
for (a.addr=0; a.addr<47; a.addr++) { // 47 instead of 48 because the last sensor (lowest down in the tank) is non-functional and always reads LOW (as if a magnet was next to it)
a.highNibble == 0 ? pinResetFast(chipInh0) : pinSetFast(chipInh0);
a.highNibble == 1 ? pinResetFast(chipInh1) : pinSetFast(chipInh1);
a.highNibble == 2 ? pinResetFast(chipInh2) : pinSetFast(chipInh2);
delay(50);
digitalWriteFast(onesBit, a.bit1);
digitalWriteFast(twosBit, a.bit2);
digitalWriteFast(foursBit, a.bit4);
digitalWriteFast(eightsBit, a.bit8);
delay(50);
int magnetReading = digitalRead(dataPin);
// interpolate between sensors if 2 are activated at the same time
if (magnetReading == 0) {
total += inchesDown[a.addr];
counter ++;
}
}
float lastValue = (indx == 0)? allData[0].reading : allData[indx - 1].reading;
float level = (counter >0)? total/counter : lastValue;
gallonsDown = level * -31.33; // there are 31.33 gallons of water per inch of height in my 8' diameter tank
if ((Time.hour() < 7 || (Time.hour() > 22 && Time.minute() > 30)) && gallonsDown - twentyThreeThirtyGallonsDown > 48 && Particle.connected()) {
Particle.publish("TankAlert", PRIVATE);
}
if (level != lastValue && indx < 99) {
allData[indx].reading = level;
allData[indx].timeStamp = Time.hour() * 60 + Time.minute();
if (shouldMonitorFillRate == true) {
shouldMonitorFillRate = false;
fillStartValue = gallonsDown;
fillRateTimer.start();
}
// get rid of repeat data if the last pair of values is the same as the pair before it
if (indx > 2) {
if (allData[indx].reading == allData[indx - 2].reading && allData[indx - 1].reading == allData[indx - 3].reading) {
allData[indx] = {0, -1};
allData[indx - 1] = {0, -1};
indx -= 2;
}
}
indx++;
if ((Time.hour() > 21 || Time.hour() < 6) && level < 0.1 && timeFilled == 0) timeFilled = Time.hour() * 60 + Time.minute();
valuesString[0] = 0;
for (int i=0; i<indx; i++) {
char s[15];
sprintf(s, "%d-%.2f, ", allData[i].timeStamp, allData[i].reading);
strcat(valuesString, s);
}
request.path = "/api/v1.6/variables/576f55be7625422b94879009/values?token=dqLW9lJbK9ilKhuhGAMHQxuCeqchrp";
request.body = "{\"value\":" + String(gallonsDown) + "}";
http.post(request, response, headers);
}
}
int fillMonitor(String cmd) {
if (cmd == "start") {
shouldMonitorFillRate = true;
return 1;
}else {
fillRateTimer.stop();
return -1;
}
}
void fillRateTimerHandler() {
if (gallonsDown == fillStartValue && abs(gallonsDown + 23.5) > 1 ) { // value stayed the same and the tank is not full
Particle.publish("WellDry", PRIVATE);
}else if (abs(gallonsDown + 23.5) < 1) { // tank is full
fillRateTimer.stop();
Particle.publish("TankFilled", PRIVATE);
}else if (gallonsDown < fillStartValue) { // tank level changed in under 5 minutes
fillStartValue = gallonsDown;
Particle.publish("FillingFast", PRIVATE);
}
}
int resetDevice(String cmd) {
shouldReset = true;
return 1;
}
int getRSSI(String cmd) {
return WiFi.RSSI();
}