Photon Remote Water Level Sensor

Pages
Contributors: wordsforthewise
Favorited Favorite 15

The Code

The code for this project lives on GitHub. You can find all the latest by following the link below.

Minimum Viable Product

In the startup world, a minimum viable product (MVP) is something that gets the job done and nothing more. I'll give you an example of the MVP for the setup we've built here.

Particle Publish and Subscribe

Using the function Particle.Publish(), we can quickly toss up a variable online so other Photons or devices can use it. In this example it will be used to send the water height and let our pump control box know the water height sensor is still online, and it can also be used to send commands, like 'turn on the pump', or 'don't deep sleep our telemetry Photon quite yet'.

The format of a publish is MQTT-like. A subscription works like a prefix filter. If you subscribe to "foo", you will receive any event whose name begins with "foo", including "foo", "fool", "foobar", and "food/indian/sweet-curry-beans".

I set up my prefix organization and subscribes as so:

language:c
Particle.subscribe("jsf/waterSystem/", eventHandler, MY_DEVICES);

If I want the water height sensor to tell the control box that it's still online, I would do:

language:c
Particle.publish("jsf/waterSystem/waterTankSensor/online", "true");

You can also use the 'private' flag with your publishes, and use the 'MY_DEVICES' flag with your subscribes, if you want to improve security.

Depending on your situation, you may be sleeping the telemetry Photon for a long time and only have it on for a few minutes or seconds. In this case, it makes updating the software tough. For that, I created a Python script that senses when the Photon comes online and tells it to wait a bit for a software update. If you want to use it, install Python -- I like using Python(x,y) for Windows, and run this script (first install sseclient, requests, and json using [pip](https://en.wikipedia.org/wiki/Pip_(package_manager)) or easy_install; type 'easy_install sseclient' or 'pip install sseclient' in your command prompt or terminal for sseclient, requests, and json):

language:python
from sseclient import SSEClient 
import requests, re, json

access_token = "YOUR ACCESS TOKEN HERE"
publish_prefix_head = "myFarm" # for subscribing to incoming messages, e.g. myFarm
publish_prefix = "myFarm/waterSystem" # e.g. myFarm/waterSystem
messages = SSEClient('https://api.spark.io/v1/events/' + publish_prefix_head + '?access_token=' + access_token)
r = requests.post('https://api.particle.io/v1/devices/events', data = {"name":publish_prefix + "/waterTankSensor/update", "data":"true", "private":"false", "ttl":"60", "access_token":access_token})
if r.json()['ok']==True:
    print 'successfully sent update request'


with open('recorded messages.txt', 'w') as record:
    for msg in messages:
        event = str(msg.event).encode('utf-8')
        data = str(msg.data).encode('utf-8')
        if re.search('jsf', event):
            dataJson = json.loads(data)
            if event == publish_prefix + '/waterTankSensor/online' and dataJson['data'] == "true":
                r = requests.post('https://api.particle.io/v1/devices/events', data = {"name":publish_prefix + "/waterTankSensor/update", "data":"true", "private":"false", "ttl":"60", "access_token":access_token})
                if r.json()['ok']==True:
                    print 'successfully sent update request'
            if event == publish_prefix + '/waterTankSensor/updateConfirm':
                if dataJson['data'] == 'waiting for update':
                    print 'device waiting for update...'
                if dataJson['data'] == 'not waiting for update':
                    print 'device no longer waiting for update.'

Save the code in a file called 'updatefirmware.py', and, once you type 'python updatefirmware.py' (from the same directory the file is located in, of course), it will print some messages out. When the device is waiting for an update, it will print 'device waiting for update...'. Then you can head to build.particle.io, and flash your device.

Telemetry Box

Create a new Photon app on build.particle.io, and include the ThingSpeak library in it (make sure it's the "ThingSpeak" library, and not the "thingspeak" library--note the capitalization). Right now it shows up as the first result from searching for 'things'. Also include the SPARKFUNMAX17043 and SPARKFUNBME280 libraries. Here is the full code:

language:c
#include "SparkFunBME280/SparkFunBME280.h"
BME280 mySensorA;
float tempF;
float pressure;
float RH;

#include "SparkFunMAX17043/SparkFunMAX17043.h"
// MAX17043 battery manager IC settings
float batteryVoltage;
float batterySOC;
bool batteryAlert;

#include "ThingSpeak/ThingSpeak.h"
//################### update these vars ###################
unsigned long myChannelNumber = your channel number here;  //e.g. 101992
const char * myWriteAPIKey = "your api key here"; // write key here, e.g. ZQV7CRQ8PLKO5QXF
//################### update these vars ###################
TCPClient client;
unsigned long lastMeasureTime = 0;
unsigned long measureInterval = 60000; // can send data to thingspeak every 15s, but give the matlab analysis a chance to add data too

// ultrasonic distance sensor for water height measurement
float uSperInch = 147; // from datasheet
float distance;
unsigned long duration;
float waterHeight;
//################### update these vars ###################
float totalDistance = 64; // the distance from the sensor to the bottom of the water tank
//################### update these vars ###################

// photocell
float lightIntensity;

// connection settings
STARTUP(WiFi.selectAntenna(ANT_EXTERNAL)); // use the u.FL antenna, get rid of this if not using an antenna
float batterySOCmin = 40.0; // minimum battery state of charge needed for short wakeup time
unsigned long wakeUpTimeoutShort = 300; // wake up every 5 mins when battery SOC > batterySOCmin
unsigned long wakeUpTimeoutLong = 900; // wake up every 15 mins during long sleep, when battery is lower
unsigned long connectedTime; // millis() at the time we actually get connected, used to see how long it takes to connect
unsigned long connectionTime; // difference between connectedTime and startTime

// for updating software
bool waitForUpdate = false; // for updating software
unsigned long updateTimeout = 600000; // 10 min timeout for waiting for software update
unsigned long communicationTimeout = 300000; // wait 5 mins before sleeping
unsigned long bootupStartTime;

// for publish and subscribe events
//################### update these vars ###################
String eventPrefix = "your prefix"; // e.g. myFarm/waterSystem
//################### update these vars ###################

bool pumpOn;

void setup() {
    // Set up the MAX17043 LiPo fuel gauge:
    lipo.begin(); // Initialize the MAX17043 LiPo fuel gauge

    // Quick start restarts the MAX17043 in hopes of getting a more accurate
    // guess for the SOC.
    lipo.quickStart();

    // We can set an interrupt to alert when the battery SoC gets too low.
    // We can alert at anywhere between 1% - 32%:
    lipo.setThreshold(20); // Set alert threshold to 20%.
    // use this to measure how long it takes to connect the Photon to the internet if you're in spotty wifi coverage
    pinMode(A0, INPUT); // ultrasonic distance sensor

    // set up BME280 sensor
    mySensorA.settings.commInterface = I2C_MODE;
    mySensorA.settings.I2CAddress = 0x77;
    mySensorA.settings.runMode = 3; //  3, Normal mode
    mySensorA.settings.tStandby = 0; //  0, 0.5ms
    mySensorA.settings.filter = 0; //  0, filter off
    //tempOverSample can be:
    //  0, skipped
    //  1 through 5, oversampling *1, *2, *4, *8, *16 respectively
    mySensorA.settings.tempOverSample = 1;
    //pressOverSample can be:
    //  0, skipped
    //  1 through 5, oversampling *1, *2, *4, *8, *16 respectively
    mySensorA.settings.pressOverSample = 1;
    //humidOverSample can be:
    //  0, skipped
    //  1 through 5, oversampling *1, *2, *4, *8, *16 respectively
    mySensorA.settings.humidOverSample = 1;
    mySensorA.begin();

    ThingSpeak.begin(client);

    Particle.subscribe(eventPrefix, eventHandler);
    Particle.publish(eventPrefix + "/waterTankSensor/online", "true"); // subscribe to this with the API like: curl https://api.particle.io/v1/devices/events/temp?access_token=1234
    bootupStartTime = millis();
    doTelemetry(); // always take the measurements at least once
}

void loop() {
    if (waitForUpdate || millis() - bootupStartTime > communicationTimeout || batterySOC > 75.0 || pumpOn) {
        // The Photon will stay on unless the battery is less than 75% full, or if the pump is running.
        // If the battery is low, it will stay on if we've told it we want to update the firmware, until that times out (updateTimeout)
        // It will stay on no matter what for a time we set, stored in the variable communicationTimeout
        if (millis() - lastMeasureTime > measureInterval) {
            doTelemetry();
        }
            if ((millis() - bootupStartTime) > updateTimeout) {
                waitForUpdate = false;
            }
    } else {
            if (batterySOC < batterySOCmin) {
                System.sleep(SLEEP_MODE_DEEP, wakeUpTimeoutLong);
            } else {
                System.sleep(SLEEP_MODE_DEEP, wakeUpTimeoutShort);
            }
    }
}

void eventHandler(String event, String data)
{
  // to publish update: curl https://api.particle.io/v1/devices/events -d "name=update" -d "data=true" -d "private=true" -d "ttl=60" -d access_token=1234
  if (event == eventPrefix + "/waterTankSensor/update") {
      (data == "true") ? waitForUpdate = true : waitForUpdate = false;
      if (waitForUpdate) {
        Serial.println("wating for update");
        Particle.publish(eventPrefix + "/waterTankSensor/updateConfirm", "waiting for update");
      } else {
        Serial.println("not wating for update");
        Particle.publish(eventPrefix + "/waterTankSensor/updateConfirm", "not waiting for update");
      }
  } else if (event == eventPrefix + "/waterTankPump/pumpOn") {
      (data == "true") ? pumpOn = true : pumpOn = false;
  }
  Serial.print(event);
  Serial.print(", data: ");
  Serial.println(data);
}

void doTelemetry() {
    // let the pump controller know we're still here
    Particle.publish(eventPrefix + "/waterTankSensor/online", "true");

    // water height
    duration = pulseIn(A0, HIGH);
    distance = duration / uSperInch; // in inches
    waterHeight = totalDistance - distance;
    ThingSpeak.setField(1, waterHeight);

    Particle.publish(eventPrefix + "/waterTankSensor/waterHeight", String(waterHeight));

    // BME280
    pressure = mySensorA.readFloatPressure()*29.529983/100000.0;
    ThingSpeak.setField(2, pressure);
    tempF = mySensorA.readTempF();
    ThingSpeak.setField(4, tempF);
    RH = mySensorA.readFloatHumidity();
    ThingSpeak.setField(5, RH);

    // photocell
    lightIntensity = analogRead(A1);
    ThingSpeak.setField(6, lightIntensity);

    // read battery states
    batteryVoltage = lipo.getVoltage();
    ThingSpeak.setField(7, batteryVoltage);
    // lipo.getSOC() returns the estimated state of charge (e.g. 79%)
    batterySOC = lipo.getSOC();
    ThingSpeak.setField(8, batterySOC);
    // lipo.getAlert() returns a 0 or 1 (0=alert not triggered)
    //batteryAlert = lipo.getAlert();

    ThingSpeak.writeFields(myChannelNumber, myWriteAPIKey); 
    lastMeasureTime = millis();
}

Variables you should change when you do this:

  • eventPrefix (e.g. myFarm/waterSystem; for publish/subscribe events)
  • myWriteAPIKey (grab from your ThingSpeak telemetry channel)
  • myChannelNumber (from ThingSpeak telemetry channel)
  • totalDistance (the distance from the sensor to the bottom of the water tank

I've bracketed these variables with:

language:c
//################### update these vars ###################

so you know what you have to change.

Additionally, you can adjust the batterySOCmin and wakeupTimeout variables if you use a different sized battery.

Control Box

Again, include the ThingSpeak library. Use this code, changing variables where applicable:

language:c
#include "ThingSpeak/ThingSpeak.h"
// channel we're writing to
//################### update these vars ###################
unsigned long myWriteChannelNumber = your channel number; // e.g 101223
const char * myWriteAPIKey = "your write API key for the pump controller channel";
//################### update these vars ###################
TCPClient client;
unsigned long lastMeasureTime = 0;
unsigned long measureInterval = 60000; // can send data to thingspeak every 15s, but once a minute is fine

bool pumpOn = false;
float waterHeight = 1000; // we want to make sure the relay isn't falsely triggered on from the get-go
//################### update these vars ###################
float lowerCutoff = 20; // lowest acceptable water height, in inches
float higherCutoff = 42; // highest acceptable water height, in inches
float totalDistance = 64; // the distance from the sensor to the bottom of the water tank
//################### update these vars ###################
int success;
unsigned long relayStartTime;
unsigned long lastSignal = millis();
unsigned long pumpTimeout = 900000; // turn off the pump if haven't heard from sensor in 15 mins
unsigned long pumpOffTime = 3600000; // make sure we don't turn on the pump more than once per hour
long pumpOffTimeStart = -pumpOffTime; // so we can turn on pump when we startup if we need to

// PIR motion sensor
int relayPin = 0;
int PIRpin = 7;
int PIRval;

// for publish and subscribe events
//################### update these vars ###################
String eventPrefix = "myFarm/waterSystem"; // e.g. myFarm/waterSystem
//################### update these vars ###################

void setup() {
    pinMode(relayPin, OUTPUT);
    pinMode(PIRpin, INPUT_PULLUP);
    digitalWrite(relayPin, LOW);

    Particle.subscribe(eventPrefix, eventHandler);

    ThingSpeak.begin(client);
}

void loop() {
    autoPumpControl();
    checkPIR();
    recordThingSpeakData();
}

int relayControl(String relayState)
{
    if (relayState == "on") {
        pumpOn = true;
        digitalWrite(relayPin, HIGH);
        relayStartTime = millis();
        ThingSpeak.setField(1, 1); // our "pump on" field
        return 1;
    }
    else if (relayState == "off") {
        pumpOn = false;
        digitalWrite(relayPin, LOW);
        ThingSpeak.setField(1, 0); // our "pump on" field
        return 1;
    }
    else {
        return 0;
    }
}

void autoPumpControl() {
    if (pumpOn) {
        if (millis() - lastSignal > pumpTimeout) { // if we haven't heard from the water tanks in a while, turn off the pump
            relayControl("off");
        }
    }
    if (waterHeight < lowerCutoff) {
        success = relayControl("on");
    } else if (waterHeight > higherCutoff) {
        success = relayControl("off");
    } else {
        ThingSpeak.setField(1, boolToNum(pumpOn)); // our "pump on" field
    }
}

void checkPIR() {
    PIRval = digitalRead(PIRpin);
    if(PIRval == LOW){
        ThingSpeak.setField(2, 1); // 1 = motion detected, 0 = no motion
    }
}

void recordThingSpeakData() {
    if (millis() - lastMeasureTime > measureInterval) {
        ThingSpeak.writeFields(myWriteChannelNumber, myWriteAPIKey);
        ThingSpeak.setField(2,0); // reset PIR motion sensor field to 'no motion detected'
        lastMeasureTime = millis();
        Particle.publish(eventPrefix + "/waterTankPump/pumpOn", boolToText(pumpOn));
    }
}

String boolToText(bool thing)
{
    String result;
    thing ? result = "true" : result = "false";
    return result;
}

int boolToNum(bool thing)
{
    int result;
    thing ? result = 1 : result = 0;
    return result;
}

void eventHandler(String event, String data)
{
  if (event == eventPrefix + "/waterTankSensor/online") {
      Particle.publish(eventPrefix + "/waterTankPump/pumpOn", boolToText(pumpOn));
  } else if (event == eventPrefix + "/waterTankSensor/online") {
      (data == "true") ? lastSignal = millis() : Serial.println(data);
  } else if (event == eventPrefix + "/waterTankSensor/waterHeight") {
      waterHeight = data.toFloat();
  }
}

Variables you should change when you do this:

  • eventPrefix (e.g. myFarm/waterSystem; for publish/subscribe events)
  • myWriteAPIKey (grab from your ThingSpeak pump controller channel)
  • myWriteChannelNumber (from ThingSpeak pump controller channel)
  • lowerCutoff (lowest acceptable water height, in inches)
  • higherCutoff (highest acceptable water height, in inches)
  • totalDistance (distance from ultrasonic sensor face to bottom of water tank)

I've bracketed these variables with:

language:c
//################### update these vars ###################

so you know what you have to change.

Some of the code may be confusing, especially something like

language:c
thing ? result = 1 : result = 0;

This is shorthand for an if-else statement. It's the equivalent of

language:c
if (thing) {
    result = 1;
} else {
    result = 0;
}

Default Firmware Feature/Bug

There are a few challenges when working with the Photon and its development environment. One of the biggest problems is that sometimes new firmware versions break old code. For example, from the time I developed this original system (Oct 2015) to the time this tutorial was written (Mar 2016), a new firmware version came out (0.4.9), which is incompatible with my old code. The worst part about it is, the Photon sits there blinking a red error message on the LED and is impossible to flash without physically accessing the device. Kind of a pain when it's on top of a roof and in a watertight enclosure held together by 6 screws (and there's a bunch of melting snow everywhere).

I think what ended up being broken with the new firmware was the WiFi.selectAntenna() function, which was silly and tiny. Regardless, we want to disable automatic firmware updates for our devices running important tasks like this, so it can keep running for years. To do this, click the 'Devices' icon on the left of build.particle.io, click the star (left) and the arrow (right) next to the device we're going to flash, then choose the 0.4.9 firmware (without Default). Re-flash your device, and it's good to go. Do this for both the telemetry and control box Photons.

just say 'no' to default firmware

Leaving the firmware on 'default' will auto-upgrade and can break your system. Just say 'no' to default firmware, and set it to 0.4.9 for this tutorial's code.