Aquarium Monitoring using the Spark Core

This tutorial will show you how to use the Spark Core to connect your fish tank to the Internet, send data to data.sparkfun.com and then pull that data down to visualize what’s happening in your fish tank using Google Charts. For this particular example, we will be monitoring the pH and the water temperature in a tank and sending a report every 10 minutes. There’s a lot to cover, so let’s get started.

Graphs


Example graphs monitoring tank parameters.

Required Materials

Here’s a list of items I used for this project. Feel free to switch out or add items as needed.

pHSparkCoreCircuit

Getting Started

The first step is to get your Core or Photon setup. The Spark website has tons of documentation for getting started. Head on over to their website to learn how to setup your device for the first time, then head on back. You should have a Core/Photon that is connected to your home wireless network, ready to accept some code. Before we program the final code onto the device, let’s build our circuit, piece by piece, testing it each step of the way.

Assembly Pt. 1

Temperature

The first step is to get the DS18B20 temperature sensor working. This has been my go-to temperature sensor because 1) It’s fairly easy to use, 2) you can connect however many you want in parallel to add more sensing to one device, 3) they can communicate over very long distances of wire, and 4) they’re completely waterproof (except where the wires are exposed). Grab the temp sensor, and connect it to the Core using the diagram below as a guide. You may need to strip and solder the exposed wire ends of the temp sensor in order for it to play nicely with the breadboard. If you are soldering together this circuit, you should be able to use the sensor as is.

SparkCoreDS18B20


Don’t forget to add the 4.7KΩ resistor in between the Vcc line (red) and the Signal line (white).

Before we can start reading the temperature, we need to first get the Address of the DS18B20 temp sensor. This sensor uses an I2C-like, 1-wire serial protocol to communicate. This means you can add multiple sensors to the same bus and can talk to each one individually using its address to identify with which sensor you’re communicating. Copy and paste the following code into the Spark IDE.

Note: You will need to add the OneWire library to this sketch in order for it to compile. For help adding libraries, please visit this page.

#include "OneWire/OneWire.h"

OneWire ds = OneWire(D2);  // on pin 10 (a 4.7K resistor is necessary)
unsigned long lastUpdate = 0; 
void setup() {
  Serial.begin(9600);
}

void loop() {
 unsigned long now = millis();
    if((now - lastUpdate) > 3000)
    {
        lastUpdate = now;
        byte i;
        byte present = 0;
        byte addr[8];

      if ( !ds.search(addr)) {
        Serial.println("No more addresses.");
        Serial.println();
        ds.reset_search();
        //delay(250);
        return;
      }
      // the first ROM byte indicates which chip
      switch (addr[0]) {
        case 0x10:
          Serial.println("Chip = DS18S20");  // or old DS1820
          break;
        case 0x28:
          Serial.println("Chip = DS18B20");
          break;
        case 0x22:
          Serial.println("Chip = DS1822");
          break;
        default:
          Serial.println("Device is not a DS18x20 family device.");
          return;
      }

      Serial.print("ROM = ");
      Serial.print("0x");
        Serial.print(addr[0],HEX);
      for( i = 1; i < 8; i++) {
        Serial.print(", 0x");
        Serial.print(addr[i],HEX);
      }

      if (OneWire::crc8(addr, 7) != addr[7]) {
          Serial.println("CRC is not valid!");
          return;
      }

    Serial.println();
      ds.reset();
    }
}

Upload the code to your device, at this time. For help using the Spark IDE, please visit this page. Once the code has been uploaded successfully, you’ll need to open a serial terminal window to read back the address of your sensor. If you are unfamiliar with serial terminals, please visit this tutorial. Take note of your devices address, and save it for later.

Ds18B20ScreenShot

In the example above, two DS18B20s are connected to one Core. If you have more than one, hook them up one at a time to distinguish which address belongs to which sensor.

Next, copy and paste the code below. You will need both the OneWire .ccp/.h files and the DallasTemperature .cpp/.h files. You can find more info on those at this github repo.

Be sure to change the Device Address variable to the address you got from the last sketch.

#pragma SPARK_NO_PREPROCESSOR

// DS18B20 Thermometer Stuff
#include "DallasTemperature.h"
#include "OneWire/OneWire.h"
#define ONE_WIRE_BUS D2
#define TEMPERATURE_PRECISION 9
OneWire oneWire(ONE_WIRE_BUS);
DallasTemperature sensors(&oneWire);

//Run I2C Scanner to get address of DS18B20(s)
DeviceAddress inWaterThermometer = { 0x28, 0xD4, 0x53, 0x60, 0x06, 0x00, 0x00, 0x9C };
//CHNAGE TO YOUR DEVICE'S ADDRESS

double InTempC = -1;
double waterTempF = -1;

void update18B20Temp(DeviceAddress deviceAddress, double &tempC);

void setup()
{
    // DS18B20 initialization
    sensors.begin();
    sensors.setResolution(inWaterThermometer, TEMPERATURE_PRECISION);

    Serial.begin(9600);   // open serial over USB
}

void loop()
{
        // DS18B20
        sensors.requestTemperatures();
        update18B20Temp(inWaterThermometer, InTempC);

        waterTempF = (InTempC * 9)/5 + 32;

        Serial.print("Water Temp:");
        Serial.print(waterTempF);
        Serial.println("F");
        delay(1000);
}

void update18B20Temp(DeviceAddress deviceAddress, double &tempC)
{
  tempC = sensors.getTempC(deviceAddress);
}

Open the serial terminal, and you should see the temperature streaming every second! Now we can add the pH circuit and calibrate our pH readings with the temperature readings.

Assembly Pt. 2

pH Circuit

Disconnect power from your circuit, and add the pH stamp using the image below as a guide.

SparkCoreDS18B20_PH

With that hooked up, we can test just the pH circuit to make sure it is functioning before we incorporate the temperature into the pH readings. Copy and paste the code below, and upload it. Once again, open the serial terminal to see the pH readings. No external libraries are needed for this example. If you experience any trouble, visit the troubleshooting section.

char ph_data[20];//we make a 20 byte character array to hold incoming data from the pH stamp.

float ph=0;//used to store the results of converting the char to float for later use. 

void setup() 
{
  Serial.begin(9600);
  Serial1.begin(38400);

    //Serial1.write("L1\r");//Uncomment to turn LEDs ON
    //delay(10);
    //Serial1.write("L0\r");//Uncomment to turn LEDs OFF
    //delay(10);
    //Serial1.print("E\r");//uncomment to stop continuous read mode
    //delay(10);
    //Serial1.print("C\r");//uncomment to start continuous read mode
    //delay(10);
}

void loop() 
{
  getPh();
  printInfo();
  delay(5000);
}

void getPh()
{
  Serial1.print("R\r");
  delay(10);
  if(Serial1.available() > 0)
  {
    Serial1.readBytesUntil(13,ph_data,20);
    ph=atof(ph_data);
  }
}

void printInfo()
{
    Serial.print("pH: ");
    Serial.println(ph);
}

Setting Up the Data Stream

Temperature. Check. pH. Check. Now it’s time to set up the data stream. Visit data.sparkfun.com. Sign up for a free account if you haven’t done so yet. Click on the Create button to create a new data stream. Once created, you will have the option to email your stream credentials. It is highly suggested you do so for reference later. Much like the Core has public key and a private key, so too does the data stream. Take note of these for use in the next step.

The Fields you choose are important as well. These will need to be referenced in the code too, so it’s best to choose names that are easy to remember.

dataStream

Final Spark Core Code

With your stream created, you can upload the final code that will live on the Core. It will push the temperature and temperature-compensated pH data to your data stream every 10 minutes, or however often you choose.

Note: You will need the OneWire and DallasTemperature library files as before, and you will also need the SparkFun Phant Library for Spark, found by searching for “Phant” in the library section of the Spark IDE.

#pragma SPARK_NO_PREPROCESSOR
#include "Phant.h"//Data.SparkFun.com Library

// DS18B20 Thermometer Stuff
#include "DallasTemperature.h"
#include "OneWire/OneWire.h"
#define ONE_WIRE_BUS D2
#define TEMPERATURE_PRECISION 9
OneWire oneWire(ONE_WIRE_BUS);
DallasTemperature sensors(&oneWire);

//Run I2C Scanner to get address of DS18B20(s)
//Replace with your sensor address(es)
DeviceAddress inWaterThermometer = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };

double InTempC = -1;
float waterTempF = 0;

//PH_VARIABLES
char ph_data[20];//20 byte character array to hold incoming data from the pH stamp.
float ph=0;//used to store the results of converting the char to float for later use. 

//PHANT STUFF
const char server[] = "data.sparkfun.com";
const char publicKey[] = "your.public.key";
const char privateKey[] = "your.private.key";
Phant phant(server, publicKey, privateKey);

//reference functions first, won't complie without this.
void update18B20Temp(DeviceAddress deviceAddress, double &tempC);
int postToPhant();
void printInfo();
void getPh();
void getTemp();
void calibrate();


void setup()
{
    // DS18B20 initialization
    sensors.begin();
    sensors.setResolution(inWaterThermometer, TEMPERATURE_PRECISION);

    Serial.begin(9600);   // open serial over USB
    Serial1.begin(38400);  //TX/RX pins on Core connected to PH Stamp

    calibrate();//throw away values
    getPh();
    postToPhant();
    delay(5000);

    //Serial1.write("L1\r");//Uncomment to turn LEDs ON
    //delay(10);
    //Serial1.write("L0\r");//Uncomment to turn LEDs OFF
    //delay(10);
    //Serial1.print("E\r");//uncomment to stop continuous read mode
    //delay(10);
    //Serial1.print("C\r");//uncomment to stop continuous read mode
    //delay(10);

}
void loop()
{
    calibrate();
    getPh();
    printInfo();
    postToPhant();
    delay(600000);//post every 10 minutes
}
int postToPhant()
{
    phant.add("temp", waterTempF);
    phant.add("ph", ph);

    TCPClient client;
    char response[512];
    int i = 0;
    int retVal = 0;

    if (client.connect(server, 80))
    {
        Serial.println("Posting!");
        client.print(phant.post());
        delay(1000);
        while (client.available())
        {
            char c = client.read();
            //Serial.print(c);
            if (i < 512)
                response[i++] = c;
        }
        if (strstr(response, "200 OK"))
        {
            Serial.println("Post success!");
            retVal = 1;
        }
        else if (strstr(response, "400 Bad Request"))
        {
            Serial.println("Bad request");
            retVal = -1;
        }
        else
        {
            retVal = -2;
        }
    }
    else
    {
        Serial.println("connection failed");
        retVal = -3;
    }
    client.stop();
    return retVal;

}
void printInfo()
{
    Serial.print("pH: ");
    Serial.print(ph);
    Serial.print(", Water Temp: ");
    Serial.print(waterTempF);
    Serial.println("F");
}
void getPh()
{
  Serial1.print("R\r");
  delay(10);
  if(Serial1.available() > 0)
  {
    Serial1.readBytesUntil(13,ph_data,20);
    ph=atof(ph_data);
  }
}
void getTemp()
{
    //get temp from DS18B20
    sensors.requestTemperatures();
    update18B20Temp(inWaterThermometer, InTempC);
    waterTempF = (InTempC * 9)/5 + 32;
}
void update18B20Temp(DeviceAddress deviceAddress, double &tempC)
{
  tempC = sensors.getTempC(deviceAddress);
}
void calibrate()
{
  getTemp();
  Serial1.print(waterTempF); //calibrate with current temp
  Serial1.write(0x0D);//carriage return
  delay(10);
}

Graphing the Data

The last step is to pull the data from the data.sparkfun stream and graph it using Google Charts. This step will require a basic understanding of HTML, JavaScript, and AJAX. However, if you are not familiar you should be able to grasp the basic understanding by copying and pasting the code below and exploring it as you alter the variables to match your data stream.

<!DOCTYPE html>
<html>
  <head>
    <!-- EXTERNAL LIBS-->
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
    <script src="https://www.google.com/jsapi"></script>

    <!-- EXAMPLE SCRIPT -->
    <script type="text/javascript">

      // load chart lib
      google.load('visualization', '1.0', {'packages': ['corechart']});

      // Set a callback to run when the Google Visualization API is loaded.
      // call drawChart once google charts is loaded
      google.setOnLoadCallback(drawCharts);

      // onload callback
      function drawCharts() {

        // JSONP request
        var jsonData = $.ajax({
          url: 'https://data.sparkfun.com/output/KJJ1rarEx3Irg7JRyaG4?gt[timestamp]=now-1%20week&timezone=America/Denver.json',
          dataType: 'jsonp',
          }).done(function (results) {

          var data1 = new google.visualization.DataTable();
          data1.addColumn('datetime', 'Time');
          data1.addColumn('number', 'H2O Temp');
          data1.addColumn('number', 'Room Temp');

          $.each(results, function (i, row) {
            data1.addRow([
              (new Date(row.timestamp)),
              parseFloat(row.H2O_temp),
              parseFloat(row.room_temp)
            ]);
          });


          var data2 = new google.visualization.DataTable();
          data2.addColumn('datetime', 'Time');
          data2.addColumn('number', 'pH');

          $.each(results, function (i, row) {
            data2.addRow([
              (new Date(row.timestamp)),
              parseFloat(row.ph)
            ]);
          });

          var options1 = {title: 'Openponics: Temp'};
          var options2 = {title: 'Openponics: pH'};

          var chart1 = new google.visualization.LineChart($('#chart').get(0));
          chart1.draw(data1, options1);

          var chart2 = new google.visualization.LineChart($('#chart2').get(0));        
          chart2.draw(data2, options2);

        });
        }
    </script>      
  </head>
  <body>
    <div id="chart"></div>
    <div id="chart2"></div>
  </body>
</html>

Conclusion

I will definitely be using the Spark Core, and its predecessor, the Photon, in many more projects to come. Using the Spark IDE took a little getting use to, but over all, I’m very pleased with the product and their cloud service. You can visit the Openponics Live Data page to see the graphs from my various charts and to get an idea of how you can set up your own data center. Thanks for reading!

Troubleshooting

  • The pH Stamp has a few gotchas when talking to it from another embedded device. Make sure the stamp is NOT in continuous mode in order for the above code to work.

  • Using the LEDs on the pH stamp as a debugging tool helps you to know if the Core is actually talking to the Stamp or not. The same goes for using Serial.print statements to help troubleshoot your code on the Core.

  • Adding libraries to the Spark IDE can take a few tries. Sometimes I just had to create the tab and add the .cpp and .h files manually (copying and pasting them from a working sketch) since the code did not want to compile otherwise.