Project Log: Ultrasonic Flood Level sensor using Photon & I2CXL-MaxSonar [MB7040]


#1

I’m looking for any example code from anyone using a MaxBotix I2CXL-MaxSonar, specifically a MB7040, with a Particle Photon.

Looks like it should be straight forward to setup, but if someone else already has it working, I’d like to leverage their code.

==========================
Background on my project:
I’m building a digital wireless stream gage to monitor the creek that flows through our farm, similar to Steve Hicks’ Ultrasonic Depth Sensor presented at Do-It-Yourself Environmental Science & Monitoring.
http://envirodiy.org/ultrasonic-water-depth-sensor/

However I am planning to post the data to the spark.io cloud every ten or fifteen minutes using a Praticle Photon with a 16 element yagi up to our barn’s WiFi (abou 100m away). [Yes I know it’s not an approved antenna. I’m a amature radio operator, KE4HET, and can operate under FCC Part 97.]


Anybody using a particle device with SDI-12 interface?
#2

@TSayles, the manufacturer has Aduino code here that you could adapt. Had you seen it?


#3

@peekay123 I’ve been poking around with that code a little.

Just loading the manufacturer’s code form the data sheet into the build.particle.io web IDE results in all kind of compiler errors. Mostly around typedef issues.
[ http://www.maxbotix.com/documents/I2CXL-MaxSonar-WR_Datasheet.pdf Pages 12 & 13. ]

The more extensive manufacturer’s sample code requires the “SoftI2CMaster.h” library which I don’t really want to port myself. http://www.maxbotix.com/documents/Arduino%20Codes/I2C_for_MaxSonar.ino

My next steps are going to fins a working Photon i2c example and modify to see if I can get it talking with the sonar sensor. Would love a suggestion of a known working i2c example I can play with.


#4

@TSayles, first, I am going to port the softI2Cmaster since I think it would be really useful for others. Once I do that, I’ll look at the sensor library to see what needs to be done there. If and when it works with the sensor and with the software I2C, we can port it to the Photon hardware I2C. :smiley:


#5

Got basic ranging functionality up and running with the following code.

/* 
Basic Range finding with I2CXL-MaxSonar
by Tom Sayles <TSayles@Soot-n-Smoke.com>

Code taken heavely from the  I2CXL-MaxSonar® - WR/WRCTM Series data sheet
http://www.maxbotix.com/documents/I2CXL-MaxSonar-WR_Datasheet.pdf
Assumes the sensor is using the default address

*/

//The Wire library uses the 7-bit version of the address, so the code example uses 0x70 instead of the 8-bit 0xE0
#define SensorAddress byte(0x70)
//The sensors ranging command has a value of 0x51
#define RangeCommand byte(0x51)
//These are the two commands that need to be sent in sequence to change the sensor address
#define ChangeAddressCommand1 byte(0xAA)
#define ChangeAddressCommand2 byte(0xA5)


void setup() {
    Serial.begin(9600); //Open serial connection at 9600 baud
    Wire.begin(); //Initiate Wire library for I2C communications with the I2CXL-MaxSonar-WR    


}

void loop() {
    
    takeRangeReading();
    //Tell the sensor to perform a ranging cycle
    delay(100);
    //Wait for sensor to finish
    uint16_t range = requestRange();
    //Get the range from the sensor
    
    String str = String(range);
    //convert to string

    Serial.print("Range String: "); Serial.println(str);
    //Print to the user

    Particle.publish("Range: ", str); 

    delay(30000);
    
}

//Commands the sensor to take a range reading
void takeRangeReading(){
    Wire.beginTransmission(SensorAddress);
    //Start addressing
    Wire.write(RangeCommand);
    //send range command
    Wire.endTransmission();
    //Stop and do something else now
}

//Returns the last range that the sensor determined in its last ranging cycle in centimeters. Returns 0 if there is no communication.
uint16_t requestRange(){
    Wire.requestFrom(SensorAddress, byte(2));
    if(Wire.available() >= 2){
        //Sensor responded with the two bytes
        byte HighByte = Wire.read();
        //Read the high byte back
        byte LowByte = Wire.read();
        //Read the low byte back
        //uint16_t range = uint16_t(HighByte, LowByte);
        uint16_t range = 256 * HighByte + LowByte;
        //Make a 16-bit word out of the two bytes for the range
        return range;
    }
    else {
        return uint16_t(0);
        //Else nothing was received, return 0
    }
}

#6

Not sure this got shared publicly, so apologies if this is a repost.

I got a message from @Julian asking how it was turning out and then “How much power does the sensor pull? How are you powering your solution?”

Below is from Dec. 16 & 17, 2015


#7

Another round of updates late this week. I had ordered a Photon Weather Shield and a from Sparkfun and they arrived yesterday.

Weather Shield https://www.sparkfun.com/products/13630
Battery Shield https://www.sparkfun.com/products/13626

So last night added i2c header pins to the Weather Shield and connected my I2CXL-MaxSonar sensor. Also hooked up a uFL to RP-SMA pigtail and a higher gain antenna.

It didn’t take much to integrate the Sparkfun libraries and example code into my working proto code. (actually it was the other way around. I restarted from the SparkFun Photon Weather Shield basic example code, added to it and modified it to fit with my earlier work on power optimization.)

Below is the code I’m running this weekend and generating data that looks like this.

So far there is a small effect of temperature on the range value, but it may be small enough I don’t need to worry about it for my application.

    /******************************************************************************
  SparkFun Photon Weather Shield basic example
  Joel Bartlett @ SparkFun Electronics
  Original Creation Date: May 18, 2015
  Updated August 21, 2015
  This sketch prints the temperature, humidity, and barometric pressure OR
  altitude to the Serial port.

  The library used in this example can be found here:
  https://github.com/sparkfun/SparkFun_Photon_Weather_Shield_Particle_Library

  Hardware Connections:
	This sketch was written specifically for the Photon Weather Shield,
	which connects the HTU21D and MPL3115A2 to the I2C bus by default.
  If you have an HTU21D and/or an MPL3115A2 breakout,	use the following
  hardware setup:
      HTU21D ------------- Photon
      (-) ------------------- GND
      (+) ------------------- 3.3V (VCC)
       CL ------------------- D1/SCL
       DA ------------------- D0/SDA

    MPL3115A2 ------------- Photon
      GND ------------------- GND
      VCC ------------------- 3.3V (VCC)
      SCL ------------------ D1/SCL
      SDA ------------------ D0/SDA

  Development environment specifics:
  	IDE: Particle Dev
  	Hardware Platform: Particle Photon
                       Particle Core

  This code is beerware; if you see me (or any other SparkFun
  employee) at the local, and you've found our code helpful,
  please buy us a round!
  Distributed as-is; no warranty is given.
*******************************************************************************/
#include "SparkFun_Photon_Weather_Shield_Library/SparkFun_Photon_Weather_Shield_Library.h"

double humidity = 0;
double tempf = 0;
double pascals = 0;
double baroTemp = 0;

long lastPublish = 0;

//Create Instance of HTU21D or SI7021 temp and humidity sensor and MPL3115A2 barometric sensor
Weather sensor;

/******************************************************************************
Basic Range finding with I2CXL-MaxSonar
by Tom Sayles <TSayles@Soot-n-Smoke.com>

Code taken heavely from the  I2CXL-MaxSonar® - WR/WRCTM Series data sheet
http://www.maxbotix.com/documents/I2CXL-MaxSonar-WR_Datasheet.pdf
Assumes the sensor is using the default address

*******************************************************************************/

//The Wire library uses the 7-bit version of the address, so the code example uses 0x70 instead of the 8-bit 0xE0
#define SensorAddress byte(0x70)
//The sensors ranging command has a value of 0x51
#define RangeCommand byte(0x51)
//These are the two commands that need to be sent in sequence to change the sensor address
#define ChangeAddressCommand1 byte(0xAA)
#define ChangeAddressCommand2 byte(0xA5)

int rangValue = 0;

int led2 = D7; // Instead of writing D7 over and over again, we'll write led2



//---------------------------------------------------------------
void setup()
{
    // Create Particle.variables for each piece of data for easy access
    Particle.variable("humidity", humidity);
    Particle.variable("tempF", tempf);
    Particle.variable("pressurePascals", pascals);
    Particle.variable("baroTemp", baroTemp);
    Particle.variable("range", rangValue);


    //Initialize the I2C sensors and ping them
    sensor.begin();

    /*You can only receive accurate barometric readings or accurate altitude
    readings at a given time, not both at the same time. The following two lines
    tell the sensor what mode to use. You could easily write a function that
    takes a reading in one made and then switches to the other mode to grab that
    reading, resulting in data that contains both accurate altitude and barometric
    readings. For this example, we will only be using the barometer mode. Be sure
    to only uncomment one line at a time. */
    sensor.setModeBarometer();//Set to Barometer Mode
    //baro.setModeAltimeter();//Set to altimeter Mode

    //These are additional MPL3115A2 functions that MUST be called for the sensor to work.
    sensor.setOversampleRate(7); // Set Oversample rate
    //Call with a rate from 0 to 7. See page 33 for table of ratios.
    //Sets the over sample rate. Datasheet calls for 128 but you can set it
    //from 1 to 128 samples. The higher the oversample rate the greater
    //the time between data samples.


    sensor.enableEventFlags(); //Necessary register calls to enble temp, baro and alt
    
    pinMode(led2, OUTPUT);
    

    publishData();

    delay(30000); // Wait for any over the air updates.

    publishData();

    delay(30000); // Wait for any over the air updates.

    publishData();

    delay(30000); // Wait for any over the air updates.

    publishData();

    delay(30000); // Wait for any over the air updates.

    digitalWrite(led2, HIGH);
    delay(500);
    digitalWrite(led2, LOW);
    delay(500);
    digitalWrite(led2, HIGH);
    delay(500);
    digitalWrite(led2, LOW);
    delay(500);
    digitalWrite(led2, HIGH);
    delay(1000);
    digitalWrite(led2, LOW);
    
    System.sleep(SLEEP_MODE_DEEP,1000); //shut down for 16 minutes and 40 seconds



}
//---------------------------------------------------------------
void loop()
{
    
    publishData();

    delay(10000);


}
//---------------------------------------------------------------


void publishData(){


    digitalWrite(led2, HIGH);

    //Get readings from all sensors
    getWeather();
    
    getRange();

    // This math looks at the current time vs the last time a publish happened
    //if(millis() - lastPublish > 30000) //Publishes every 5000 milliseconds, or 5 seconds
    //{
        // Record when you published
        lastPublish = millis();
        
        // Choose which values you actually want to publish- remember, if you're
        // publishing more than once per second on average, you'll be throttled!
        Particle.publish("humidity", String(humidity));
        Particle.publish("tempF", String(tempf));
        
        //Particle.variable("pressurePascals", pascals);
        //Particle.variable("baroTemp", baroTemp);
        
        Particle.publish("Range: ", String(rangValue)); // Publish a range event
        
    //}
    
    digitalWrite(led2, LOW);

}



void getWeather()
{
  // Measure Relative Humidity from the HTU21D or Si7021
  humidity = sensor.getRH();

  // Measure Temperature from the HTU21D or Si7021
  tempf = sensor.getTempF();
  // Temperature is measured every time RH is requested.
  // It is faster, therefore, to read it from previous RH
  // measurement with getTemp() instead with readTemp()

  //Measure the Barometer temperature in F from the MPL3115A2
  baroTemp = sensor.readBaroTempF();

  //Measure Pressure from the MPL3115A2
  pascals = sensor.readPressure();

  //If in altitude mode, you can get a reading in feet with this line:
  //float altf = sensor.readAltitudeFt();
}

void takeRangeReading(){
    Wire.beginTransmission(SensorAddress);
    //Start addressing
    Wire.write(RangeCommand);
    //send range command
    Wire.endTransmission();
    //Stop and do something else now
}

void getRange() {


    takeRangeReading();
    //Tell the sensor to perform a ranging cycle
    delay(100);
    //Wait for sensor to finish
    uint16_t range = requestRange();
    //Get the range from the sensor
    

    rangValue = range;  //update the online range varable

    Serial.print("Range String: "); Serial.println(String(rangValue));
    //Print to the user
    
}


//Returns the last range that the sensor determined in its last ranging cycle in centimeters. Returns 0 if there is no communication.
uint16_t requestRange(){
    Wire.requestFrom(SensorAddress, byte(2));
    if(Wire.available() >= 2){
        //Sensor responded with the two bytes
        byte HighByte = Wire.read();
        //Read the high byte back
        byte LowByte = Wire.read();
        //Read the low byte back
        //uint16_t range = uint16_t(HighByte, LowByte);
        uint16_t range = 256 * HighByte + LowByte;
        //Make a 16-bit word out of the two bytes for the range
        return range;
    }
    else {
        return uint16_t(0);
        //Else nothing was received, return 0
    }
}

#8

Since my last update I’ve added a SparkFun Photon Battery Shield, https://www.sparkfun.com/products/13626, and a 2 minute on, 18 minute off update cadence. Together with a 4000 mAh Li-ion battery, I’ve been able to run almost 4.5 days on about 50% of the charge.

This gives me high confidence I can with a couple 6000 mAh 18650 Li-ion batteries in parallel achieve my stated goal of a flood monitor that can run for a week without being recharged, with a good margin.

Also I’ve done a fair bit of informal testing now between 60 and 80 cm at various temperatures and haven’t seen more than a centimeter or two deviation from the mean when placed on a hard flat surface. Which indicates for my application, I can probably get away without doing any temperature compensation. :grinning:

Note that the range (blue line) drop on Christmas eve & day was when stuck a jar at the bottom of the pipe. Also mid-day on 12/27 were a couple changes in location. Most notably from inside my heated apartment to the loft of our (wood frame) barn and then about 24 hours later to the loft of our “Coverall™” arena.
December 21, 22, 23, 26 were travel days when no data was collected. December 23, 24, 25 were at my brother’s place in southern California. The rest were at the farm where I live in western Washington (between Seattle and the Cascade Mtns.)


#9

Up next, in no particular order:

  • Fabricate enclosure from 4" (green) PVC sewer pipe and fittings for initial field testing;
  • Fabricate calibration test rig from ~15’ of 4" (green) PVC sewer pipe and fittings for controlled range calibration and temperature sensitivity testing with actual water;
  • Setup off-line / near-line IDE with proper revision control [git?] and software testing;
    Anyone have suggestions on a good setup for working from a Debian / Linux Mint desktop?
  • Refactor prototype firmware code to provide better remote health information, diagnostics, and modularity;
  • Explore cloud data solutions that might be better (more efficient) than Colud Variables and Google Spreadsheets; [https://data.sparkfun.com/ ? Phant?];
  • Build (or find people to help build) a front end webpage with pretty graphs for flood “Stage” [elevation - range] showing key flood levels for our property, and “Rate of Rise” showing how fast the water is rising or falling, as well as some reasonable diagnostics like RSSI and Battery State of Charge.

#10

I’ll be interested to see how the 4" pipe works. I did some testing with some long pieces of 3" and 2" pipe and I couldn’t get it to work with them. It would either detect minimum or maximum range. But when I tried a piece of 1.5" (which has the same inner diameter as the cone on the sensor), it worked fine. I think it must have been echoing inside the other pipes but somehow the 1.5" worked. Hopefully the 4" will be big enough not to cause problems for you.
I currently have 2 sensors like that (only mine are serial output) mounted but I wound up not putting either one inside of a pipe. The first one has been measuring flow through a wastewater treatment plant for over a year and a half with no problems. It’s connected to an Arduino and logging readings to an SD card. The second one has been mounted on the side of a bridge for a couple of months measuring the water level of a stream. It’s connected to an Arduino and sending the readings via XBee to a Core (at least until I get an Electron to replace that setup). I have the Core set up with a webhook that sends the readings to a Firebase database. It’s not that hard to graph the data stored in Firebase. That one is not currently online (and hasn’t been in a while), but that graph will update with live data as soon as it is sent to Firebase. I haven’t had time to make more than that simple graph, but I’m going to have to get it fully working pretty soon, so maybe I’ll have something more useful to share before too long.


#11

Life and weather gets in the way, so I haven’t managed to get the field testing I was planning up and going yet. I decided I needed to make some structural integrity improvements to the internals and also wanted to add a better battery pack.


Built a nice big [18650, 6000mAH x 4] battery pack for use with my IoT projects.
by Tom Sayles, on Flickr

Still working on the rebuild of the internals and enclosure. The temporary cardboard bulkhead has been replaced with an acrylic[?] disk. I’ve got all the parts I want for the field testing in house now. So it’s just a matter of finding the time to put it all together and setup the tests.

I did manage to extract my prototype code development from build.Particle.io to my local machine (using the Atom editor with Particle packages) and push to a GitHub repository.
Pull requests welcome!!

I have also decided that I will use https://data.sparkfun.com for cloud data storage, and have started playing with pushing data to streams there.


#12

I have managed to do some bench testing with the 4" pipe. So far, with ranges from about 30 cm to about 100 cm, I am getting good readings, as long there are not any joints in the pipe. 0.5" holes in the pipe seem to be fine. I have plans to set up a 15’ [5m] vertical stand pipe with a hose attachment and valve to do field verification testing and any calibration I need. Once I have this setup I will post pictures, probably on this project share.


#13

Interesting project! I look forward to seeing more of your results from testing different tubes. I recently installed a similar project tracking the water level in our shared multi-household water tank. I integrated:

This all fits in a waterproof outlet box with a hinged, clear plastic cover that’s mounted up under the eaves of our tank’s wooden roof. The PV panel mounts adjacent to it off the end of one of the rafters. The range sensor is inside the tank at the end of a PVC pipe.

At startup I sample range, temp & humidity then send to https://thingspeak.com/channels/61103 via WiFi. Then it goes into deep sleep for 30 minutes. It’s been running for almost a month and seems fine on power despite the rain & clouds we’ve had and only seeing direct sunlight for ~2 hours/day.

I see more drift in the reported range (as indicated by volume) than I do when physically looking into the tank. That could be due to splashing from the fill pipe which is likely a bit too close to the sensor. I’ll relocate the sensor soon in hopes of stabilizing the values. If it continues I may substitute the waterproof model you’re using or consider a pipe as a shield. I’m using the Maxbotix analog voltage output (pin 3).

I traded email with someone developing commercial stream gauges using pressure transducers. I considered that option as well but decided the ultrasonic range sensor should suffice in the closed environment of our tank.

Once I have more time with it, I’ll post a note with photos here as a Project Share. Looking forward to updates on your project as well.

Credit where due – I’d been thinking about this for some time, but seeing this inspired me to build it: http://www.instructables.com/id/Wi-Fi-Twitter-Water-Level-Sensor/


#14

The M3 nylon standoffs in that I was waiting for arrived, and I found time to drill out more holes in my acrylic bulkhead.

Note that this layout is for a custom laser cut finial design and doesn’t exactly match my hand cut prototype. The main difference is that my handmade prototype started from an off the shelf 4" disk.

The standoffs provide a stable and solid structure, so I’m not worried about anything coming unplugged inside.

All the fittings are to be sealed with o-rings. The gore vent allows pressure equalization while keeping dirt water etc. out of the enclosure.

Still need to work up a proper mast for the yagi antenna, but this is getting close to what I intend it to look like in the field.


#15

The first field test was a little rushed Monday morning as the NOAA Flood forecast increased overnight to the point I wanted to track this flood event. Kind of thrown together with duct tape and zip ties.

Thought I had it working before I had to head into work (at the day job) but it wasn’t connecting to the barn WiFi, about 100m away. As I thought about the problem during the day, I realized that the barn had several tons of hay that wasn’t there for my earlier scouting experiments. After work I raised the barn access point about 2m so it could “see” over the stack of hay bails, and the Photon immediately connected.

I setup a webhook and and it has been pushing data to data.sparkfun.io for the past few days.
https://data.sparkfun.com/streams/n1EQXJJb6mUzDLV52dN5

When I get a chance I will push the latest firmware source and (sanitized) webhook JSON to GitHub.

Note that about 10 AM PST today, the water level in the creek dropped below the base of the stilling pipe and I think the mud that the pipe was sitting on was giving some funny echos. and since about 9 PM PST this evening the whole gage assembly, including the ~9’ stilling pipe, has been relocated to the hayloft [sitting on a hard floor]. The plan is to do some additional development and calibration bench testing.


#16

I created a Bill Of Material list, that includes some sourcing information.


#17

Hi Tom,
just wondering if you had any insights on how well it worked. I saw on the sparkfun data the output is periodically jumping a little.
many thanks


#18

Sorry I haven’t posted any updates.
I’ve joined a small team being lead by the Snoqualmie Valley Preservation Alliance. This past fall and winter [2016-2017] we assembled ~12 prototypes, basically following the design above, with Electron instead of Photon.
The major accomplishment for the field trial seems to have been chasing the large winter storm events that cause our flooding down to California.
We did get a few units in the field where most worked well enough for first round prototypes. Main issues were working on include:

  • acoustics [standing wave echos? related to mounting?]
  • implement over sampling & outlier removal
  • backend data processing
  • user front end
  • Radio use optimizations
  • battery pack design fabrication
  • firmware optimization for battery life

We are hopeful that adding some over sampling & outlier removal will smooth out our collected data.

Firmware is open source and available at: https://github.com/snovalleypa/SVPA-FloodSensor-2016

Electron based sensor working atop a 20’ stilling pipe.


#19

Stage vs. Time during one of the few flood events we had this past winter.


#20

Gosh Many thanks for the pics and detail.
Interesting the 20’ stilling pipe.
I’m working as part of a larger project on stream monitoring in california. Also working on just those issues. So be interested to collaborate where possible.