Smart Cities - Setting up Aquarium sensor in Tech School

From Sensors in Schools
Jump to navigation Jump to search

Aquarium sensor in Tech School

Overview

In this lesson we are going to set up a sensor in the Whittlesea Tech School aquarium. The sensors will measure water temperature, conductivity and dissolved oxygen. The sensor data will be available using an API that can be accessed from a Raspberry Pi.

A Particle electron microcontroller has been programmed to read the sensors. The Particle electron is connected to the 3G network and the sensor data is available on the Particle server. Webhooks have been created in the Particle console so send the data to the bio2lab.eagle.io server. The bio2lab.eagle-io server is then able to present the data in a Dashboard accessible to the public on the internet. As an added function, the bio2lab.eagle.io server also makes the sensor data available through an API. This API can be accessed by students running python programs.

The lesson today will explore how to write these APIs in Python on a Raspberry Pi computer.

Learning Objectives

  • Learn how to get sensors data using an API
  • Learn how to process sensors data and save data
  • Learn how to include sensor data in a simple HTML web page

Dashboard

A link has been provided to the bio2lab.eagle.io Dashboard. The Dashboard is presenting live sensor data from the Whittlesea Tech School aquarium. The data is updated every 15 minutes.

Live sensor data include:

  • water temperature
  • water conductivity (salts in water)
  • Water dissolved oxygen
  • water level (depth sensor) - to be connected
  • water turbidity - to be connected
  • battery voltage
  • battery state of charge (soc)

Aquarium techschool Dashboard

Collecting battery voltage sensor data using API

  • Copy the following code by highlighting the code then right mouse button click and select copy.

  • Open the IDLE3 Python3 editor.
  • Select New File.
  • Past the copied code in the New File.
  • Save the file as aquarium_bat.py


Explanation of aquarium_bat.py code

A brief explanation of the python code follows:

  • A unique node_id is required to access data specific for a sensor. To access the battery voltage data we use the node_id aquarium_bat = '624a51b9d3b9f6dc855eaaaa'
  • We prepare a request.get command (r_bat = requests.get(url = req_uri + aquarium_bat, headers = req_headers)) which will get the battery data and place the response in the r_bat object. We can work with this object (package) later to extract the data.
  • If we run the code print(r_bat) it will tell us if our request has been successful or not. An output in the Python shell of <Response 200> indicates that the get request has executed correctly.
  • The statement print(r_bat.json()['currentValue']) uses a JSON method to make battery sensor data more accessible. We are only interested in the currentValue attribute.

Python code aquarium_bat.py

#!/usr/bin/env python3

# Import packages
import requests
import json

aquarium_bat = '624a51b9d3b9f6dc855eaaaa'
#624a51b9d3b9f6dc855eaaa0 - state of charge
#624a51b9d3b9f6dc855eaaaa - battery voltage
#624a51b9d3b9f6dc855eaaa4 - dissolved oxygen
#624a51b9d3b9f6dc855eaaac - conductivity
#624a51b9d3b9f6dc855eaaa2 - internal humidity
#624a51b9d3b9f6dc855eaaa6 - internal temperature
#624a51b9d3b9f6dc855eaaae - outside temperature


api_key           = 'Ti74NO5dur5Zs8WiVXPEo7VP5VA0PRrvIFmURTzK'
req_uri          = 'https://api.eagle.io/api/v1/nodes/'

req_headers       = {'Content-Type': 'application/json', 'X-Api-Key': api_key}

r_bat = requests.get(url = req_uri + aquarium_bat, headers = req_headers)

print(r_bat)
print("Battery voltage")
print(r_bat.json()['currentValue'])

bat = str(r_bat.json()['currentValue'])
print(bat)

Python Shell output for aquarium_bat.py

The code above will send the following output to the Python Shell.

Collecting other sensor data using API

In this extension of the code we have expanded the number of sensor data node_id points. The full data collection now includes:

  • battery state of change (soc)
  • battery voltage
  • dissolved oxygen in the aquarium (should be between 6 and 12mg/L)
  • temperature in the aquarium (degC)
  • conductivity in the aquarium (salt levels)
  • temperature in the sensor housing (ambient)
  • humidity in the sensor housing (ambient)

Python code for aquarium_all_sensors.py

  • Copy the code below into a new file in IDLE3.
  • Save the file as aquarium_all_sensors.py
  • Run the file.
#!/usr/bin/env python3
#aquarium_all_sensors.py

# Import packages
import requests
import json

aquarium_soc = '624a51b9d3b9f6dc855eaaa0' # battery state of charge (soc)
aquarium_bat = '624a51b9d3b9f6dc855eaaaa' # battery voltage
aquarium_dox = '624a51b9d3b9f6dc855eaaa4' # dissolved oxygen in the aquarium water
aquarium_temp = '624a51b9d3b9f6dc855eaaae' # temperature of the aquarium water
aquarium_cond = '624a51b9d3b9f6dc855eaaac' # conductivity (salt levels) in the aquarium water
aquarium_int_temp = '624a51b9d3b9f6dc855eaaa6' # temperature of the air in the sensor housing
aquarium_int_hum = '624a51b9d3b9f6dc855eaaa2' # humidity of the air in the sensor housing

api_key           = 'Ti74NO5dur5Zs8WiVXPEo7VP5VA0PRrvIFmURTzK'
req_uri          = 'https://api.eagle.io/api/v1/nodes/'

req_headers       = {'Content-Type': 'application/json', 'X-Api-Key': api_key}

r_bat = requests.get(url = req_uri + aquarium_bat, headers = req_headers)
r_soc = requests.get(url = req_uri + aquarium_soc, headers = req_headers)
r_dox = requests.get(url = req_uri + aquarium_dox, headers = req_headers)
r_temp = requests.get(url = req_uri + aquarium_temp, headers = req_headers)
r_cond = requests.get(url = req_uri + aquarium_cond, headers = req_headers)
r_int_temp = requests.get(url = req_uri + aquarium_int_temp, headers = req_headers)
r_int_hum = requests.get(url = req_uri + aquarium_int_hum, headers = req_headers)

print(r_bat)
print("Battery voltage")
print(r_bat.json()['currentValue'])
print("State of Charge")
print(r_soc.json()['currentValue'])
print("Dissolved oxygen")
print(r_dox.json()['currentValue'])
print("Aquarium temp")
print(r_temp.json()['currentValue'])
print("Aquarium conductivity - salt")
print(r_cond.json()['currentValue'])
print("Aquarium internal temperature")
print(r_int_temp.json()['currentValue'])
print("Aquarium internal humidity")
print(r_int_hum.json()['currentValue'])

Python Shell output for aquarium_all_sensors.py

Simplifying Print statements

  • We can modify the code so that the print statements are more consolidated.
  • Now the explanation of the data and the sensor data will appear in a single line of code.
  • Copy the code below and test it.
#!/usr/bin/env python3
#aquarium_all_sensors_print.py

# Import packages
import requests
import json

aquarium_soc = '624a51b9d3b9f6dc855eaaa0' # battery state of charge (soc)
aquarium_bat = '624a51b9d3b9f6dc855eaaaa' # battery voltage
aquarium_dox = '624a51b9d3b9f6dc855eaaa4' # dissolved oxygen in the aquarium water
aquarium_temp = '624a51b9d3b9f6dc855eaaae' # temperature of the aquarium water
aquarium_cond = '624a51b9d3b9f6dc855eaaac' # conductivity (salt levels) in the aquarium water
aquarium_int_temp = '624a51b9d3b9f6dc855eaaa6' # temperature of the air in the sensor housing
aquarium_int_hum = '624a51b9d3b9f6dc855eaaa2' # humidity of the air in the sensor housing

api_key           = 'Ti74NO5dur5Zs8WiVXPEo7VP5VA0PRrvIFmURTzK'
req_uri          = 'https://api.eagle.io/api/v1/nodes/'

req_headers       = {'Content-Type': 'application/json', 'X-Api-Key': api_key}

r_bat = requests.get(url = req_uri + aquarium_bat, headers = req_headers)
r_soc = requests.get(url = req_uri + aquarium_soc, headers = req_headers)
r_dox = requests.get(url = req_uri + aquarium_dox, headers = req_headers)
r_temp = requests.get(url = req_uri + aquarium_temp, headers = req_headers)
r_cond = requests.get(url = req_uri + aquarium_cond, headers = req_headers)
r_int_temp = requests.get(url = req_uri + aquarium_int_temp, headers = req_headers)
r_int_hum = requests.get(url = req_uri + aquarium_int_hum, headers = req_headers)

print(r_bat)
print("Battery voltage", r_bat.json()['currentValue'])
print("State of Charge", r_soc.json()['currentValue'])
print("Dissolved oxygen", r_dox.json()['currentValue'])
print("Aquarium temp", r_temp.json()['currentValue'])
print("Aquarium conductivity - salt", r_cond.json()['currentValue'])
print("Aquarium internal temperature", r_int_temp.json()['currentValue'])
print("Aquarium internal humidity", r_int_hum.json()['currentValue'])

Shell Output for aquarium_all_sensors_print.py

The output below shows that sensor descriptions and data appear on the same line of output.

You can also add units to the end of the data values.

Which will produce the following output in the Python Shell.

Saving sensor data to data.txt file

To keep a simple record of the history of sensor data we can save the compiled data in a text file. We can review the data later by opening the text file in a simple editor.

Create empty data.txt file

Python code for aquarium_data_save.py

  • Create a New File in IDLE3.
  • Copy the code below and paste it in the empty file.
  • Save the file as aquarium_data_save.py
  • Run the code.

Explanation of Python code aquarium_data_save.py

  • The statement data = bat + "," + soc + "," + dox + "," + temp + "," + cond + "," + int_temp + "," + int_hum + "\n" takes all the individual sensor data variables and concatenates them together in a single String. The long data string is assigned to the variable named data.
  • Note that if a line of code is too long it can be divided into two using the special backslash character \.
  • A new file Object' is created that points to data.txt f = open('/home/pi/techschool/data.txt','a'). The a option means that new data will be added to the end of the file (a = append).
  • The data variable is written to the file f.write(data)
  • And then the file object is closed f.close()
#!/usr/bin/env python3
#aquarium_data_save.py

# Import packages
import requests
import json

aquarium_soc = '624a51b9d3b9f6dc855eaaa0' # battery state of charge (soc)
aquarium_bat = '624a51b9d3b9f6dc855eaaaa' # battery voltage
aquarium_dox = '624a51b9d3b9f6dc855eaaa4' # dissolved oxygen in the aquarium water
aquarium_temp = '624a51b9d3b9f6dc855eaaae' # temperature of the aquarium water
aquarium_cond = '624a51b9d3b9f6dc855eaaac' # conductivity (salt levels) in the aquarium water
aquarium_int_temp = '624a51b9d3b9f6dc855eaaa6' # temperature of the air in the sensor housing
aquarium_int_hum = '624a51b9d3b9f6dc855eaaa2' # humidity of the air in the sensor housing

api_key           = 'Ti74NO5dur5Zs8WiVXPEo7VP5VA0PRrvIFmURTzK'
req_uri          = 'https://api.eagle.io/api/v1/nodes/'

req_headers       = {'Content-Type': 'application/json', 'X-Api-Key': api_key}

r_bat = requests.get(url = req_uri + aquarium_bat, headers = req_headers)
r_soc = requests.get(url = req_uri + aquarium_soc, headers = req_headers)
r_dox = requests.get(url = req_uri + aquarium_dox, headers = req_headers)
r_temp = requests.get(url = req_uri + aquarium_temp, headers = req_headers)
r_cond = requests.get(url = req_uri + aquarium_cond, headers = req_headers)
r_int_temp = requests.get(url = req_uri + aquarium_int_temp, headers = req_headers)
r_int_hum = requests.get(url = req_uri + aquarium_int_hum, headers = req_headers)

print(r_bat)
print("Battery voltage", r_bat.json()['currentValue'], "V")
print("State of Charge", r_soc.json()['currentValue'], "%")
print("Dissolved oxygen", r_dox.json()['currentValue'], "mg/L")
print("Aquarium temp", r_temp.json()['currentValue'], "degC")
print("Aquarium conductivity - salt", r_cond.json()['currentValue'], "mS/cm")
print("Aquarium internal temperature", r_int_temp.json()['currentValue'], "degC")
print("Aquarium internal humidity", r_int_hum.json()['currentValue'], "%")

bat = str(r_bat.json()['currentValue'])
soc = str(r_soc.json()['currentValue'])
dox = str(r_dox.json()['currentValue'])
temp = str(round(r_temp.json()['currentValue'], 2))
#temp = str(r_temp.json()['currentValue'])
cond = str(r_cond.json()['currentValue'])
int_temp = str(r_int_temp.json()['currentValue'])
int_hum = str(r_int_hum.json()['currentValue'])

data = bat + "," + soc + "," + dox + "," + temp + "," + cond + "," +\
       int_temp + "," + int_hum + "\n"
print(data)

print("Saving data in data.txt file")
f = open('/home/pi/techschool/data.txt','a')
f.write(data)
f.close()

Shell Output for aquarium_data_save.py

Creating a dynamic web page

#!/usr/bin/env python3

# Import packages
import requests
import json

aquarium_soc = '624a51b9d3b9f6dc855eaaa0'
aquarium_bat = '624a51b9d3b9f6dc855eaaaa'
aquarium_dox = '624a51b9d3b9f6dc855eaaa4'
aquarium_temp = '624a51b9d3b9f6dc855eaaae'
aquarium_int_hum = '624a51b9d3b9f6dc855eaaa2'
aquarium_int_temp = '624a51b9d3b9f6dc855eaaa6'
aquarium_cond = '624a51b9d3b9f6dc855eaaac'
#624a51b9d3b9f6dc855eaaa0 - state of charge
#624a51b9d3b9f6dc855eaaaa - battery voltage
#624a51b9d3b9f6dc855eaaa4 - dissolved oxygen
#624a51b9d3b9f6dc855eaaac - conductivity
#624a51b9d3b9f6dc855eaaa2 - internal humidity
#624a51b9d3b9f6dc855eaaa6 - internal temperature
#624a51b9d3b9f6dc855eaaae - outside temperature


api_key           = 'Ti74NO5dur5Zs8WiVXPEo7VP5VA0PRrvIFmURTzK'
req_uri          = 'https://api.eagle.io/api/v1/nodes/'

req_headers       = {'Content-Type': 'application/json', 'X-Api-Key': api_key}

r_bat = requests.get(url = req_uri + aquarium_bat, headers = req_headers)
r_soc = requests.get(url = req_uri + aquarium_soc, headers = req_headers)
r_dox = requests.get(url = req_uri + aquarium_dox, headers = req_headers)
r_temp = requests.get(url = req_uri + aquarium_temp, headers = req_headers)
r_int_hum = requests.get(url = req_uri + aquarium_int_hum, headers = req_headers)
r_int_temp = requests.get(url = req_uri + aquarium_int_temp, headers = req_headers)
r_cond = requests.get(url = req_uri + aquarium_cond, headers = req_headers)

print(r_bat)
print("Battery voltage")
print(r_bat.json()['currentValue'])
print("State of Charge")
print(r_soc.json()['currentValue'])
print("Dissolved oxygen")
print(r_dox.json()['currentValue'])
print("Aquarium temp")
print(r_temp.json()['currentValue'])
print("Aquarium internal humidity")
print(r_int_hum.json()['currentValue'])
print("Aquarium internal temperature")
print(r_int_temp.json()['currentValue'])
print("Aquarium conductivity - salt")
print(r_cond.json()['currentValue'])


bat = str(r_bat.json()['currentValue'])
soc = str(r_soc.json()['currentValue'])
dox = str(r_dox.json()['currentValue'])
temp = str(round(r_temp.json()['currentValue'], 2))
#temp = str(r_temp.json()['currentValue'])
int_hum = str(r_int_hum.json()['currentValue'])
int_temp = str(r_int_temp.json()['currentValue'])
cond = str(r_cond.json()['currentValue'])

data = bat + "," + soc + "," + dox + "," + temp + "," +\
       int_hum + "," + int_temp + "," + cond + "\n"
print("Data compiled")
print(data)

print("Saving data in data.txt file")
f = open('/home/pi/techschool/data.txt','a')
f.write(data)
f.close()

print("Creating HTML file")
message = """
<h1>Techschool Aquarium Home Page</h1>
<p>Environmental data on the Techschool Aquarium</p>
"""
f = open('/home/pi/techschool/index.html','w')
f.write(message)
f.close()

Dynamic web page displaying temperature data

#!/usr/bin/env python3

# Import packages
import requests
import json
import html

aquarium_soc = '624a51b9d3b9f6dc855eaaa0'
aquarium_bat = '624a51b9d3b9f6dc855eaaaa'
aquarium_dox = '624a51b9d3b9f6dc855eaaa4'
aquarium_temp = '624a51b9d3b9f6dc855eaaae'
aquarium_int_hum = '624a51b9d3b9f6dc855eaaa2'
aquarium_int_temp = '624a51b9d3b9f6dc855eaaa6'
aquarium_cond = '624a51b9d3b9f6dc855eaaac'
#624a51b9d3b9f6dc855eaaa0 - state of charge
#624a51b9d3b9f6dc855eaaaa - battery voltage
#624a51b9d3b9f6dc855eaaa4 - dissolved oxygen
#624a51b9d3b9f6dc855eaaac - conductivity
#624a51b9d3b9f6dc855eaaa2 - internal humidity
#624a51b9d3b9f6dc855eaaa6 - internal temperature
#624a51b9d3b9f6dc855eaaae - outside temperature


api_key           = 'Ti74NO5dur5Zs8WiVXPEo7VP5VA0PRrvIFmURTzK'
req_uri          = 'https://api.eagle.io/api/v1/nodes/'

req_headers       = {'Content-Type': 'application/json', 'X-Api-Key': api_key}

r_bat = requests.get(url = req_uri + aquarium_bat, headers = req_headers)
r_soc = requests.get(url = req_uri + aquarium_soc, headers = req_headers)
r_dox = requests.get(url = req_uri + aquarium_dox, headers = req_headers)
r_temp = requests.get(url = req_uri + aquarium_temp, headers = req_headers)
r_int_hum = requests.get(url = req_uri + aquarium_int_hum, headers = req_headers)
r_int_temp = requests.get(url = req_uri + aquarium_int_temp, headers = req_headers)
r_cond = requests.get(url = req_uri + aquarium_cond, headers = req_headers)

print(r_bat)
print("Battery voltage")
print(r_bat.json()['currentValue'])
print("State of Charge")
print(r_soc.json()['currentValue'])
print("Dissolved oxygen")
print(r_dox.json()['currentValue'])
print("Aquarium temp")
print(r_temp.json()['currentValue'])
print("Aquarium internal humidity")
print(r_int_hum.json()['currentValue'])
print("Aquarium internal temperature")
print(r_int_temp.json()['currentValue'])
print("Aquarium conductivity - salt")
print(r_cond.json()['currentValue'])


bat = str(r_bat.json()['currentValue'])
soc = str(r_soc.json()['currentValue'])
dox = str(r_dox.json()['currentValue'])
temp = str(round(r_temp.json()['currentValue'], 2))
#temp = str(r_temp.json()['currentValue'])
int_hum = str(r_int_hum.json()['currentValue'])
int_temp = str(r_int_temp.json()['currentValue'])
cond = str(r_cond.json()['currentValue'])

data = bat + "," + soc + "," + dox + "," + temp + "," +\
       int_hum + "," + int_temp + "," + cond + "\n"
print("Data compiled")
print(data)

print("Saving data in data.txt file")
f = open('/home/pi/techschool/data.txt','a')
f.write(data)
f.close()

print("Creating HTML file")
message = """
<h1>Techschool Aquarium Home Page</h1>
<p>Environmental data on the Techschool Aquarium</p>
<p>The temperature of the aquarium is %s</p>
""" % (html.escape(str(temp)))
f = open('/home/pi/techschool/index.html','w')
f.write(message)
f.close()

Adding more data to a Dynamic web page

#!/usr/bin/env python3

# Import packages
import requests
import json
import html

aquarium_soc = '624a51b9d3b9f6dc855eaaa0'
aquarium_bat = '624a51b9d3b9f6dc855eaaaa'
aquarium_dox = '624a51b9d3b9f6dc855eaaa4'
aquarium_temp = '624a51b9d3b9f6dc855eaaae'
aquarium_int_hum = '624a51b9d3b9f6dc855eaaa2'
aquarium_int_temp = '624a51b9d3b9f6dc855eaaa6'
aquarium_cond = '624a51b9d3b9f6dc855eaaac'
#624a51b9d3b9f6dc855eaaa0 - state of charge
#624a51b9d3b9f6dc855eaaaa - battery voltage
#624a51b9d3b9f6dc855eaaa4 - dissolved oxygen
#624a51b9d3b9f6dc855eaaac - conductivity
#624a51b9d3b9f6dc855eaaa2 - internal humidity
#624a51b9d3b9f6dc855eaaa6 - internal temperature
#624a51b9d3b9f6dc855eaaae - outside temperature


api_key           = 'Ti74NO5dur5Zs8WiVXPEo7VP5VA0PRrvIFmURTzK'
req_uri          = 'https://api.eagle.io/api/v1/nodes/'

req_headers       = {'Content-Type': 'application/json', 'X-Api-Key': api_key}

r_bat = requests.get(url = req_uri + aquarium_bat, headers = req_headers)
r_soc = requests.get(url = req_uri + aquarium_soc, headers = req_headers)
r_dox = requests.get(url = req_uri + aquarium_dox, headers = req_headers)
r_temp = requests.get(url = req_uri + aquarium_temp, headers = req_headers)
r_int_hum = requests.get(url = req_uri + aquarium_int_hum, headers = req_headers)
r_int_temp = requests.get(url = req_uri + aquarium_int_temp, headers = req_headers)
r_cond = requests.get(url = req_uri + aquarium_cond, headers = req_headers)

print(r_bat)
print("Battery voltage")
print(r_bat.json()['currentValue'])
print("State of Charge")
print(r_soc.json()['currentValue'])
print("Dissolved oxygen")
print(r_dox.json()['currentValue'])
print("Aquarium temp")
print(r_temp.json()['currentValue'])
print("Aquarium internal humidity")
print(r_int_hum.json()['currentValue'])
print("Aquarium internal temperature")
print(r_int_temp.json()['currentValue'])
print("Aquarium conductivity - salt")
print(r_cond.json()['currentValue'])


bat = str(r_bat.json()['currentValue'])
soc = str(r_soc.json()['currentValue'])
dox = str(r_dox.json()['currentValue'])
temp = str(round(r_temp.json()['currentValue'], 2))
#temp = str(r_temp.json()['currentValue'])
int_hum = str(r_int_hum.json()['currentValue'])
int_temp = str(r_int_temp.json()['currentValue'])
cond = str(r_cond.json()['currentValue'])

data = bat + "," + soc + "," + dox + "," + temp + "," +\
       int_hum + "," + int_temp + "," + cond + "\n"
print("Data compiled")
print(data)

print("Saving data in data.txt file")
f = open('/home/pi/techschool/data.txt','a')
f.write(data)
f.close()

print("Creating HTML file")
message = """
<h1>Techschool Aquarium Home Page</h1>
<p>Environmental data on the Techschool Aquarium</p>
<p>The temperature of the aquarium is %s</p>
<p>The conductivity of the aquarium is %s</p>
""" % (html.escape(str(temp)), html.escape(str(cond)))
f = open('/home/pi/techschool/index.html','w')
f.write(message)
f.close()


Appendix - Particle electron code for Aquarium-tech

// Example script for field deployed particle device - monitoring water quality using Atlas 
// Scientific sensors mounted on a Whitebox Labs Tentacle
// Ensure all jumpers on the tentacle board are set to I2C and all ezo boards are also set to I2C
// Consult the tentacle and atlas documentation for further details 

#include <Adafruit_DHT_Particle.h>
#include <RunningMedianST.h>
#include <JsonParserGeneratorRK.h>
#include <Wire.h>                  //enable I2C.

#define DHTTYPE DHT22		// DHT 22 (AM2302)
#define DHTPIN D3           // what pin we're connected to

#define PRESSPIN A0 // A0 will be used for pressure sensor
#define TURBPIN A1 // Pin for reading turbidity (changed from A0 by Edmond 11 Jan 2022)

#define PWR5V D2 // Pin to turn on power to DHT, Atlas sensors, turbidity and pressure sensor
//#define PWR12V D7 // Pin to turn on power to turbidity sensor - in my case this is a 12V power supply - yours may be something different

SYSTEM_MODE(AUTOMATIC);

DHT dht(DHTPIN, DHTTYPE);

FuelGauge fuel;

double powerSource;
double batteryState;
double BatteryVoltage;
double BatterySOC;

byte code = 0;                   //usedanalogRead to hold the I2C response code.
byte in_char = 0;                //used as a 1 byte buffer to store in bound bytes from the EZO Circuit.
byte i = 0;                      //counter used for IC2 data
int delay_time = 1800;           //used to change the delay needed depending on the command sent to the EZO circuit.
int address;
char ezo_data[48];                //we make a 20 byte character array to hold incoming data from the EZO circuit. 

String T_string;                        
String EC_string; 
//String pH_string; 
String DO_string;

String T_status;                        
String EC_status; 
//String pH_status; 
String DO_status; 
String T_int_string;
String H_int_string;

double DateTime;
double Tvalue;                                  
double ECvalue;   
//double pHvalue;
double DOvalue;
double NTUV; 
double Pvalue;

float internal_Tvalue;
float internal_Hvalue;

//Sampling time 
int SampleTime = 30;        //Sample time in seconds
int SleepTime = 15;         //minutes to sleep before waking again / target is 15 minutes in the field / 60 minutes during testing
int StartTime;  
int elapsedTime;

RunningMedianFloat floatSamples = RunningMedianFloat(10);  // number of samples to take for each composite sample - water quality
RunningMedianFloat floatSamples_int_T = RunningMedianFloat(4);   // number of samples to take for each composite sample - internal temperature
RunningMedianFloat floatSamples_int_H = RunningMedianFloat(4);   // number of samples to take for each composite sample - internal humidity
RunningMedianFloat floatSamples_NTUV = RunningMedianFloat(10); 
RunningMedianFloat floatSamples_int_P = RunningMedianFloat(10); 

//SystemSleepConfiguration config;


////////////////////////////////////////////////////////////////////////////////////////////////
// Comment out when not using aquarium-tech
//PMIC pmic;

void setup()                     //hardware initialization.
{
  //pmic.begin();
  //pmic.setChargeCurrent(0,0,0,0,0,0);
    
  Serial.begin(9600);            //enable serial port.  
  //SerialLogHandler logHandler(LOG_LEVEL_ALL); 
  
  //ApplicationWatchdog wd(660000, System.reset); //This Watchdog code will reset the processor if the dog is not kicked every 11 mins which gives time for 2 modem reset's. 

  //set system power configuration 
  SystemPowerConfiguration conf;

  conf.powerSourceMaxCurrent(550) 
      .powerSourceMinVoltage(4840) 
      .batteryChargeCurrent(1024) 
      .batteryChargeVoltage(4210);

  // comment out for aquarium-tech
  System.setPowerConfiguration(conf); 
 
  //Log.info("setPowerConfiguration=%d", res);
 
  
 Particle.variable("BatteryVoltage", BatteryVoltage);
 Particle.variable("BatterySOC", BatterySOC);
 Particle.variable("powerSource", powerSource);    
 Particle.variable("batteryState", batteryState);  
 Particle.variable("temperature", Tvalue);
 Particle.variable("conductivity", ECvalue);
 Particle.variable("disOxygen", DOvalue);
 Particle.variable("turbidity", NTUV);
 Particle.variable("pressure",  Pvalue);
 Particle.variable("internalTemp", T_int_string);
 Particle.variable("internalHumid", H_int_string);
 
  Wire.begin();                  //enable I2C port.
  
  pinMode(PWR5V, OUTPUT); 

}

//Main Loop
////////////////////////////////////////////////////////////////////////////////////////////////
void loop() 
{   

    //check at startup there is enough power
/////////////////////////////////////////////////////////////////////////
    
    Serial.println("Checking power source");
    int powerSource = System.powerSource();
    Serial.printlnf("Power source = %i", powerSource);

    Serial.println("Checking battery state");
    int batteryState = System.batteryState();
    Serial.printlnf("Battery state = %i", batteryState);
 
    Serial.println("Checking battery level");
    BatteryVoltage = fuel.getVCell();
    BatterySOC = fuel.getNormalizedSoC();
    
    Serial.print("Battery Voltage: "); 
    Serial.println(BatteryVoltage);
    
    Serial.print("Battery State of Charge: "); 
    Serial.println(BatterySOC);
    
    // More sophisticated battery control for field use - commented out for testing purposes


    if (BatterySOC < double(20))
        {
        Serial.println("Battery below 20% : will sleep for 12 hours to charge battery");
        SleepTime = (12*60); 
        }
    else if (BatterySOC < double(40))
        {
        Serial.println("Battery below 40% : will sleep for 6 hours to charge battery");
        SleepTime = (6*60);    
        } 
    else if (BatterySOC < double(60))
        {
        Serial.println("Battery below 60% : will sleep for 2 hours to charge battery");
        SleepTime = (2*60);  // 2 hours
        }
    else if (BatterySOC < double(80))
        {
        Serial.println("Battery below 80% : will sleep for 1 hour to charge battery");
        SleepTime = (1*60);  // 2 hours
        }
    else
        {
        Serial.printlnf("Battery level OK - above 80% - will sleep for %i minutes", SleepTime);
        delay(2000);    
        }
   
/////////////////////////////////////////////////////////////////////////   
 
    Serial.println(("Turn on power to sensors"));      //turn on power to sensors
    digitalWrite(PWR5V, HIGH); 
        
     //Initialise DHT temperature adn humidity sensor
    Serial.println("initialising temp and humidity sensor DHT22");
	dht.begin();
	delay(2000);
	
	//Temp and Humidity
////////////////////////////////////////////////////////////////////////////////////////////////////////////     
    StartTime = Time.now();
    Serial.print("Begin temperature and humidity sampling at: ");
    Serial.println(StartTime);
    
    floatSamples_int_T.clear();
    floatSamples_int_H.clear();

    while (Time.now() - StartTime < 6)
        {
            
        internal_Tvalue = dht.getTempCelcius();
        internal_Hvalue = dht.getHumidity();
        
        // Check if any reads failed and exit early (to try again).
	    if (isnan(internal_Hvalue) || isnan(internal_Tvalue) ) 
	        {
		    Serial.println("Failed to read from DHT sensor!");
		    //T_int_string = "no read";
	        }
	        else
	        {
            Serial.printlnf("Spot temperature = %f, Spot humidity = %f", internal_Tvalue, internal_Hvalue);
            floatSamples_int_T.add(internal_Tvalue);
            floatSamples_int_H.add(internal_Hvalue);
            //T_int_string = "yes read";
	        }
        
        delay(2000);
        }
        
    Serial.print("Finished temperature and humidity sampling at: ");
    Serial.println(Time.now());
    
    internal_Tvalue = floatSamples_int_T.getMedian();
    internal_Hvalue = floatSamples_int_H.getMedian();
    T_int_string = String(internal_Tvalue);
    H_int_string = String(internal_Hvalue);
    
    Serial.printlnf("Median temperature = %f, Median humidity = %f", internal_Tvalue, internal_Hvalue);

//Water Quality - Temperature
//////////////////////////////////////////////////////////////////////////////////////////////////////////// 	
   ezoCommand("W", 102);  
    Serial.println(("checking T board status: "));
    T_status = ezoCommand("Status", 102);        //Check the status
    Serial.print("T status: ");
    Serial.println(T_status);
    
    StartTime = Time.now();    
    Serial.print("Begin T sampling at: ");
    Serial.println(StartTime);
    floatSamples.clear();
    
    while (Time.now() - StartTime < SampleTime/3)
        {
        T_string = ezoCommand("R",102);
        Tvalue = atof(T_string);
        Serial.print("Spot T = ");
        Serial.println(Tvalue);
        floatSamples.add(Tvalue);
        delay(1000);
        }
        
    Serial.print("Finished T sampling at: ");
    Serial.println(Time.now());
    
    delay(200);
    Tvalue = floatSamples.getMedian();
    Serial.print("Median T = ");
    Serial.println(Tvalue);
    
    T_string = String(Tvalue);

//Water Quality - Conductivity    
////////////////////////////////////////////////////////////////////////////////////////////////////////////
    Serial.println(("checking EC board status: "));
    EC_status = ezoCommand("STATUS",100);        //Check the status
    Serial.print(("EC status: "));
    Serial.println(EC_status);
    delay(500);
    
    // Serial4.println("K,0.1");  // set conductivity probe to 0.1
    ezoCommand("K,0.1",100);   // Set conductivity probe to K0.1 - default is K1.0
    delay(500);
    
    Serial.print("Setting EC temperature compensation at: ");
    Serial.println(Tvalue);
    ezoCommand(String("T,"+T_string), 100);
    delay(500);
    
    StartTime = Time.now();    
    Serial.print("Begin EC sampling at: ");
    Serial.println(StartTime);
    floatSamples.clear();
    
    while (Time.now() - StartTime < SampleTime)
        {
        EC_string = ezoCommand("R",100);
        ECvalue = atof(EC_string);
        Serial.print("Spot EC = ");
        Serial.println(ECvalue);
        floatSamples.add(ECvalue);
        delay(1000);
        }
        
    Serial.print("Finished EC sampling at: ");
    Serial.println(Time.now());
    
    delay(200);
    ECvalue = floatSamples.getMedian();
    Serial.print("Median EC = ");
    Serial.println(ECvalue);
/*
//Water Quality - pH    
////////////////////////////////////////////////////////////////////////////////////////////////////////////
    Serial.println(("checking pH board status: "));
    pH_status = ezoCommand("STATUS",99);        //Check the status
    Serial.print(("pH status: "));
    Serial.println(pH_status);
    
    Serial.print("Setting pH temperature compensation at: ");
    Serial.println(Tvalue);
    ezoCommand(String("T,"+T_string), 99);
    delay(500);
    
    StartTime = Time.now();    
    Serial.print("Begin pH sampling at: ");
    Serial.println(StartTime);
    floatSamples.clear();
    
    while (Time.now() - StartTime < SampleTime)
        {
        pH_string = ezoCommand("R",99);
        pHvalue = atof(pH_string);
        Serial.print("Spot pH = ");
        Serial.println(pHvalue);
        floatSamples.add(pHvalue);
        delay(1000);
        }
        
    Serial.print("Finished pH sampling at: ");
    Serial.println(Time.now());
    
    delay(200);
    pHvalue = floatSamples.getMedian();
    Serial.print("Median pH = ");
    Serial.println(pHvalue);
    */

//Water Quality - Dissolved Oxygen    
////////////////////////////////////////////////////////////////////////////////////////////////////////////  

    Serial.println(("checking DO board status: "));
    DO_status = ezoCommand("STATUS",97);        //Check the status
    Serial.print(("DO status: "));
    Serial.println(DO_status);
    
    Serial.print("Setting DO temperature compensation at: ");
    Serial.println(Tvalue);
    ezoCommand(String("T,"+T_string), 97);
    delay(500);
    
    StartTime = Time.now();    
    Serial.print("Begin DO sampling at: ");
    Serial.println(StartTime);
    floatSamples.clear();
    
    while (Time.now() - StartTime < SampleTime)
        {
        DO_string = ezoCommand("R",97);
        DOvalue = atof(DO_string);
        Serial.print("Spot DO = ");
        Serial.println(DOvalue);
        floatSamples.add(DOvalue);
        delay(1000);
        }
        
    Serial.print("Finished DO sampling at: ");
    Serial.println(Time.now());
    
    delay(200);
    DOvalue = floatSamples.getMedian();
    Serial.print("Median DO = ");
    Serial.println(DOvalue);
  

  // Turbidity
  ////////////////////////////////////////////////////////////////////////////////////////////////////////////

    StartTime = Time.now();
    Serial.print("Begin turbidity sampling at: ");
    Serial.println(StartTime);
    
    floatSamples_NTUV.clear();
    
    while (Time.now() - StartTime < SampleTime)
        {
            
        NTUV = analogRead(TURBPIN);
    
        Serial.print("NTU: ");
        Serial.println(NTUV);
        
        floatSamples_NTUV.add(NTUV);
        
        delay(500);
        
        }
    
    
    Serial.print("Finished turbidity sampling at: ");
    Serial.println(Time.now());
    
    delay(500);
    
    NTUV = floatSamples_NTUV.getMedian();
    Serial.print("Median turbidity: ");
    Serial.println(NTUV);
    
    delay(1000);
    
    
 //  Pressure transducer - Water level sensor
  ////////////////////////////////////////////////////////////////////////////////////////////////////////////

    StartTime = Time.now();

    floatSamples_int_P.clear();
    
    while (Time.now() - StartTime < SampleTime)
        {
            
        Pvalue = analogRead(PRESSPIN);
    
        floatSamples_int_P.add(Pvalue);
        
        delay(500);
        }
    
    
    delay(500);
    
    Pvalue = floatSamples_int_P.getMedian();
    Serial.print("Median pressure: ");
    Serial.println(Pvalue);
    
    delay(20000);
    
    Serial.println("Turning off power to all sensors");
    digitalWrite(PWR5V, LOW); 
    

//Sens data to particle cloud
////////////////////////////////////////////////////////////////////////////////////////////////////////////
  Serial.println("Preparing sensor data");
  delay (1000);
  
  Serial.printlnf("Sensor data - Temp: %f EC: %f DO: %f NTUV: %f Pressure: %f Internal Temp: %f Internal Hum: %f Battery Voltage: %f Battery Charge: %f", Tvalue, ECvalue, DOvalue, NTUV, Pvalue, internal_Tvalue, internal_Hvalue, BatteryVoltage, BatterySOC);
  Serial.printlnf("Sensor status - T Status: %s EC Status: %s", T_status.c_str(), EC_status.c_str());
  delay (1000);
  
  Serial.println("Uploading sensor data");
  createPayload(DateTime, Tvalue, ECvalue, DOvalue, NTUV, Pvalue, internal_Tvalue, internal_Hvalue, BatteryVoltage, BatterySOC);
  delay (3000); 
  
 //Serial.println("Turning off cellular modem");
 //Cellular.off();
  

  Serial.printlnf("Going to sleep for %i minutes" , SleepTime);
  //System.sleep(config); 
  
  //************
  // Commented out sleep (following line)during testing (Edmond 11 Jan 2022)
  // Comment out following line for aquarium-tech so that the battery can charge
  // Note that deep sleep prevents charging of battery when powered using USB supply
  //System.sleep(SLEEP_MODE_DEEP, (SleepTime * 60)); // wake up every 15 minutes
  
  // 60 x 15 second delay between readings = 15 minute power down between readings
  // Comment out delay for normal operation (i.e. solar powered operation)
  delay(60000*15);
  //***************
  
}

// Function Definitions
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

// Read from sensors    
////////////////////////////////////////////////////////////////////////////////////////

String ezoCommand(String command, int address)
{

    Wire.beginTransmission(address);        //call the circuit by its ID number.  
    Wire.write(command);                    //transmit the command that was sent through the serial port.
    Wire.endTransmission();                 //end the I2C data transmission. 

    delay(delay_time); 

    Wire.requestFrom(address,48,1);        //call the circuit and request 20 bytes (this may be more than we need)
    code=Wire.read(); 

    while(Wire.available())
        {                                    //are there bytes to receive.  
        in_char = Wire.read();              //receive a byte.
        ezo_data[i]= in_char;               //load this byte into our array.
        i+=1;                               //incur the counter for the array element. 
        if(in_char==0)
            {                               //if we see that we have been sent a null command. 
            i=0;                            //reset the counter i to 0.
            Wire.endTransmission();         //end the I2C data transmission.
            break;                          //exit the while loop.
            }
        }

    switch (code){                  //switch case based on what the response code is.  
      case 1:                       //decimal 1.  
        Serial.print("Success - ");  //means the command was successful.
      break;                        //exits the switch case.

     case 2:                        //decimal 2. 
       Serial.print("Failed");    //means the command has failed.
     break;                         //exits the switch case.

     case 254:                      //decimal 254.
       Serial.print("Pending");   //means the command has not yet been finished calculating.
     break;                         //exits the switch case.

     case 255:                      //decimal 255.
       Serial.print("No Data");   //means there is no further data to send.
     break;                         //exits the switch case.
    }

    if (code == 1){
    return ezo_data ;
    } 
    else {
      return "";
    }      
}

//JSON generator function for sensor data
///////////////////////////////////////////////////////

void createPayload(double DateTime, double Tvalue, double ECvalue, double DOvalue, double NTUV, double Pvalue, double internal_Tvalue, double internal_Hvalue, double BatteryVoltage, double BatterySOC)
{
 JsonWriterStatic<256> jw;
 {
   JsonWriterAutoObject obj(&jw);
   jw.insertKeyValue("DateTime", Time.now());
   jw.insertKeyValue("T", Tvalue);
   jw.insertKeyValue("EC", ECvalue);
   jw.insertKeyValue("DO", DOvalue);
   jw.insertKeyValue("NTUV", NTUV);
    jw.insertKeyValue("pressure", Pvalue);
   jw.insertKeyValue("internal_T", internal_Tvalue);
   jw.insertKeyValue("internal_H", internal_Hvalue);
   jw.insertKeyValue("BatteryVoltage", BatteryVoltage);
   jw.insertKeyValue("BatterySOC", BatterySOC);
 }
 
 Particle.publish("PayloadJSON", jw.getBuffer(), PRIVATE);
}