Water quality sensor

From Sensors in Schools
Jump to navigation Jump to search

Water Quality sensor

Background and Applications

Water quality sensors are used to monitor the health of waterways and waterbodies. The sensors can monitor:

  • temperature - in summer water temperatures will rise. High temperatures are harmful to aquatic animals. Shade from trees planted along river banks (riparian zone) helps to keep temperatures low. Rain showers coming off hot roads and roofs can produce stormwater that is much warmer.
  • conductivity - level of salt in fresh water. Salt levels in waterways normally increase in summer because there is less water to dilute naturally occuring salts. Salts can also arise from disturbed soil.
  • dissolved oxygen - low levels of dissolved oxygen result in fish kill. High levels of organic material in water can reduce dissolved oxygen levels, because as bacteria in the water feed on the organic material they remove oxygen from the water. Very deep water (>3m) may also have deeper, cooler lox oxygen zones (anoxic zones). Mixing the water and keeping water shallow helps to reduce the occurence of anoxic zones.
  • turbidity - a measure of the cloudiness of the water. The more cloudy the higher the turbidity. Turbidity normally results from land disturbance and the removal of vegetation.
  • water level - aquatic animals suffer when water levels are too high (water flowing fast during and after a rain event) or when water levels are too low (cease to flow events) during summer. Urban environments are terrible for aquatic animals because we have significantly changed water flow patterns. In the past water would land on the ground and take time to soak into the clay soils. River Red gum trees and soils would recharge during these wet periods. Today, with roads, roofs and other hard surfaces, we take water away from the landscape too quickly because we have added stormwater pipes. All this excess water is then directed to local waterways where it creates flooding issues. Ideally waterways would prefer to have only modest changes to the water level, more reflective of the seasonal changes in water availability. In fact, more waterways were fed from graoundwater rather than surface water.

Key design considerations

  • The design for this water quality sensor circuit and the software was provided by Bio2Lab.
  • Water temperature, conductivity, pH and dissolved oxygen probes from Atlas Scientific.
  • The water level sensor is a pressure sensor from Altas Scientific. Water pressure can be used to determine water level height. Other sensors based on ultrasonic distance measurements could also be used.
  • Turbidity probe is a low cost analog sensor adapted to be a proxy for turbidity. More accurate turbidity sensors are still too expensive.
  • A Particle electron microcontroller was used to power the sensors and connect to the 3G network. A Particle Boron or a Pycom GPy micrcontroller could have also been used. A flat 3G antenna is mounted within the sensor housing.
  • A Freetronics ProtoShield Basic for Arduino (Core Electronics) was used to build the circuitry so that it could be plugged into the Whitebox T1 sensor shield.
  • To save space on the ProtoSheild the Particle electron microcontroller has only half the pins plugged in (with the other pins hanging over the edge of the board).
  • The microcontroller is powered using a 6V 3.4 Watt Solar panel (Adafruit).
  • The solar panel is able to feed directly into the VIN pin on the Particle electron. The Particle electron internal regulator is able to manage (P = VI) up to 0.57 A of peak current being delivered by the solar panel. Based on product documentation the input voltage range on VIN pin is 3.9VDC to 12VDC.
  • A 3.7V 6000 mA LiPo battery is used for power storage (Core Electronics). The battery is connected directly to the battery plug on the Particle electron. The battery is charged via the Particle electron internal regulator.
  • Both the battery and the solar panel can be turned on/off (isolated) using a Pushbutton Switch 6A@250VAC Neon IP56DPST switch (Jaycar electronics).
  • The Particle electron provides a PIN (VBAT) that gives access to the battery plugged into the micrcontroller.
  • There is no need for a voltage divider circuit because the Particle electron has a inbuilt function to measure battery voltage.
  • The output of the VBAT pin is fed to a Pololu 5V Step-Up Step-Down Voltage Regulator S7V8F5 (Core Electronics) to supply a 5V supply for the sensors. The benefit of this regulator is that is has a Shutdown (SHDN) PIN that turns the regulator on/off based on a digital signal (requires pull-down 10k resistor). An electrolytic capacitor (25V 100uF) was added across the VIN and GND pins of the Pololu voltage regulator to help prevent voltage spikes damaging the regulator during when the circuit is powered up.
  • Temperature, conductivity and dissolved oxygen sensors are connected to a Whitebox T1 sensor shield (Atlas Scientific). Communication is via I2C. A 3.3V reference voltage is supplied from the 3.3V PIN on the Particle electron.
  • Turbidity and water level sensors are powered using a 5V regulated supply and send their analog output to the analog input pins on the Particle electron. Note - the Particle electron Analog pins are sensitive to over-voltage (above 3.3V).
    • The water level sensor voltage output will not exceed 3.3V with water levels between 0 and 2.0m.
    • The turbidity sensor (5V supply) will deliver 4.8V in clear water. The circuit includes a voltage divider circuit to divide the voltage output by half. Two high precision 10k ohm resistors (<0.1% variation) were used for the voltage divider circuit.
  • An additional internal temperature and humidity (DHT22) sensor is mounted with the sensor housing to monitor for the ingress of moisture or excessive temperature extremes. A pull-up resistor was added to the data line.

Atlas Whitebox T1

  • The Whitebox T1 is powered using the Arduino power setting (not the external USB power setting).
  • Power is supplied to the IOREF pin. The voltage supply may be either 3.3V or 5V.
  • Whitebox T1 pin 5V or VIN are not used.
  • GND must be connected.
  • If the Whitebox T1 is powered (via IOREF) at 3.3V then all data output lines will be at 3.3V. If powered at 5V all data output lines will be at 5V. The receiving microcontroller must be tolerant of these voltages.
    • Particle electron microcontrollers (3.3V device) are tolerant of 5V inputs into Digital pins.
    • Pycom microcontrollers (3.3V devices) are not tolerant to 5V inputs.

Circuit diagram

  • The circuit diagram is largely correct, but does not include additional details on:
    • electrolytic capacitor (25V 100uF) added to the voltage regulator across the VIN and GND pins to protect the Pololu voltage regulator.
    • pull-up resistor on the data pin for the internal temp/humidity (DHT22) sensor
    • voltage divider circuit for the turbidity sensor circuit
    • the circuit diagram incorrectly shows the 5V pin on the Whitebox T1 (Tentacle shield) being supplied 5V to the 5V pin. The 5V pin and the VIN pins on the Whitebox T1 are not used. Supply is only provided via the IOREF pin.

    • Note that the Particle electron is a 3.3V device, however the digial pins are tolerant to 5V data inputs.

Description of each water quality sensor

The following notes describe some of the important specifications for each water quality

Temperature sensor PT-1000 - Atlas Scientific

Overview

  • Cable length 81cm / 6mm cable diameter.
  • Life expectancy 15 years.
  • Uses EZO RTD Temperature Circuit to measure temperature and communicate with Whitebox T1 and microcontrollers.
  • Can be used in UART and I2C communication modes.
  • Documentation for EZO RTD Temperature Circuit

Important notes

  • Ensure that the EZO RTD circuit is switched to either UART or I2C mode.
  • Instructions to convert from UART to I2C mode
  • To read the temperature sensor using an Arduino Uno refer to Atlas Scientific documentation
  • To read the temperature sensor using the Whitebox T1 shield connected to an Arduino Uno refer to this documentation Whitebox T1 documentation

Conductivity probe sensor K 0.1 - Atlas Scientific

Overview

Important notes

  • There is also a Conductivity probe K 1.0. However this probe is used to measure higher levels of salinity and is not suitable for this application.
  • Ensure that the EZO RTD circuit is switched to either UART or I2C mode.
  • Instructions to convert from UART to I2C mode
  • To read the temperature sensor using an Arduino Uno refer to Atlas Scientific documentation
  • To read the temperature sensor using the Whitebox T1 shield connected to an Arduino Uno refer to this documentation Whitebox T1 documentation


Atlas Scientific EZO Circuit mode change to I2C

  • For the EZO RTD Temperature circuit connect the following:
    • VCC to 5V
    • GND to ground
    • bridge (short) TX and PRB
  • Power up and wait until the EZO circuit turns blue.
  • Disconnect the bridge
  • Power down.


  • For the EZO Conductivity and EZO Dissolved Oxygen circuit
    • VCC to 5V
    • GND to ground
    • bridge (short) TX and PGND
  • Power up and wait until the EZO circuit turns blue.
  • Disconnect the bridge
  • Power down.
  • Note: some of the EZO circuits are incorrectly labelled, however the pin arrangement is correct.

DHT22 Internal temperature sensor

Overview

  • The DHT22 temperature and humidity sensor that is placed within the sensor housing.
  • Normally this sensor is used to detect abnormally high or low temperatures, or excess humidity.
  • Extreme conditions can reduce the functional life of the sensor.
  • The following link is a good reference to the use of the sensor. [[1]]

Important notes

  • DHT22 Pin layout
    • Pin 1 is VCC (Power Supply)
    • Pin 2 is DATA (The data signal)
    • Pin 3 is NULL (Do not connect)
    • Pin 4 is GND (Ground)
  • The DHT22 is a slow sensor which requires up to 2 seconds to take a reading
  • It can operate at 3.3V or 5V, but 5V is preferable.
  • The DHT22 requires a 10K pull-up resistor between pins 1 (VCC) and 2 (DATA). Failure to do this will result in data loss.

Software installation requirements

  • Arduino IDE installed on PC or Mac to test the operation of the sensors using an Arduino Uno.

Parts list

Item description Number Supplier Part number URL Cost Notes
Solder-able Breadboard 1 Core Electronics SKU: PRT-12070 https://core-electronics.com.au/solder-able-breadboard.html $8.20 55mm x 80mm
Pycom LoPy4 (LoRaWAN) 1 Core Electronics SKU: CE05399 https://core-electronics.com.au/pycom-lopy4.html $68.95 Microcontroller with LoRa
Pycom Expansion Board 3.1 1 Core Electronics SKU: CE05545 https://core-electronics.com.au/pycom-expansion-board-3-1.html $35.95 Used to program LoPy4
DHT22 Temperature and Relative Humidity Sensor Module 1 Core Electronics SKU: 018-DHT22 https://core-electronics.com.au/dht22-temperature-and-relative-humidity-sensor-module.html $13.75 Temp precision +/-0.5degC
Temperature Sensor - Waterproof (DS18B20) 1 Core Electronics SKU: SEN-11050 https://core-electronics.com.au/temperature-sensor-waterproof-ds18b20.html $18.80 1-Wire interface library
Electrolytic Decoupling Capacitors - 100uF/25V 1 Core Electronics SKU: COM-00096 https://core-electronics.com.au/electrolytic-decoupling-capacitors-100uf-25v.html $0.29 Used to block high voltage spikes
Pololu 5V Step-Up Voltage Regulator U1V11F5 1 Core Electronics SKU: POLOLU-2562 https://core-electronics.com.au/pololu-5v-step-up-voltage-regulator-u1v11f5.html $11.65 5V supply for sensors
Resistor 10K Ohm 1/4 Watt PTH - 20 pack (Thick Leads) 1 Core Electronics SKU: PRT-14491 https://core-electronics.com.au/resistor-10k-ohm-1-4-watt-pth-20-pack-thick-leads.html $2.05 Pack of 20 resistors
Hook-Up Wire - Assortment (Solid Core, 22 AWG) 1 Core Electronics SKU: PRT-11367 https://core-electronics.com.au/hook-up-wire-assortment-solid-core-22-awg.html $40.95 Solid core for wiring circuit
36-pin 0.1 Female header - pack of 5! 1 Core Electronics SKU: ADA598 https://core-electronics.com.au/20-pin-0-1-female-header-pack-of-5.html $6.30 Used for plugin components
10 Pcs 40 Pin Headers - Straight 1 Core Electronics SKU: FIT0084 https://core-electronics.com.au/10-pcs-40-pin-headers-straight.html $4.70 Used to make sensors plugin
Holman 100mm PVC DWV Push On Cap 1 Bunnings DWVF0198 https://www.bunnings.com.au/holman-100mm-pvc-dwv-push-on-cap_p4770360 $3.30 End caps for sensor housing
Holman 100mm x 3m PVC DWV Pipe 1 Bunnings DWV1003 https://www.bunnings.com.au/holman-100mm-x-3m-pvc-dwv-pipe_p4770345 $33.00 Cut in store for easier transport
Selleys 310g Clear Glass Silicone Sealant 1 Bunnings Model no 9300697100382 https://www.bunnings.com.au/selleys-310g-clear-glass-silicone-sealant_p1234951 $15.00 Used with caulking gun
18650 2600mAh Li-ion Protected Battery 1 Jaycar CAT.NO: SB2299 https://www.jaycar.com.au/18650-2600mah-li-ion-protected-battery/p/SB2299 $21.95 Inbuilt safety protection
Single 18650 Battery Holder 1 Jaycar CAT.NO: PH9205 https://www.jaycar.com.au/single-18650-battery-holder/p/PH9205 $3.25 Battery holder
Dual-Channel Li-ion / Ni-MH Battery Charger 1 Jaycar CAT.NO: MB3635 https://www.jaycar.com.au/dual-channel-li-ion-ni-mh-battery-charger/p/MB3635 $39.95 Only one required for all projects
MOLEX 211140-0100.. 868/915MHZ FLEXIBLE ANTENNA100MM CABLE 1 Element-14 Order code 3225220 https://au.element14.com/molex/211140-0100/868-915mhz-flexible-antenna100mm/dp/3225220?ost=molex++211140-0100..++868%2F915mhz+flexible+antenna100mm+cable $2.84 Attached to LoPy4
Through Hole Resistor, 10 kohm, CMF, 500 mW, ± 0.1%, Axial Leaded, 250 V 2 Element-14 Order code 3596907 https://au.element14.com/vishay/cmf5510k000beea/res-10k-0-5w-axial-metal-film/dp/3596907?ost=vishay++cmf5510k000beea $1.56 High precision resistor for voltage divider
P9406 • 6 Pin 5A Screw-On Female Line IP66 Waterproof Socket 1 Altronics Code: P9406 https://www.altronics.com.au/p/p9406-amphenol-ltw-6-pin-5a-screw-on-female-line-ip66-waterproof-socket/ $23.50 Connected to sensor housing
P9416 • 6 Pin 5A Screw-On Male Chassis IP67 Waterproof Plug 1 Altronics Code P9416 https://www.altronics.com.au/p/p9416-amphenol-ltw-6-pin-5a-screw-on-male-chassis-ip67-waterproof-plug/ $17.25 Connected to sensor


Testing Atlas sensors with Whitebox T1 and Arduino Uno

Hardware Setup

  • The temperature, conductivity and dissolved oxygen sensors can be tested using the Whitebox T1 and Arduino Uno.
  • Connect the sensors as shown.

  • Close up of Whiteboard T1 shield.
  • Important notes:
    • Whiteboard T1 power mode is switched to Ardunio (not external).
    • The alignment of the EZO circuits and the sensor connector is slightly offset.
  • Port connections
    • Port 1 - not connected
    • Port 2 - temperature sensor
    • Port 3 - conductivity sensor
    • Port 4 - dissolved oxygen sensor


Software Setup

Arduino code has been collapsed by default. Click here to expand:

// Whitebox Interactive EZO Console
// https://www.whiteboxes.ch/
//
// Tool to help you setup Atlas Scientific EZO devices
//
// THIS CODE IS NOT INTENDED AS A BOILERPLATE FOR YOUR PROJECT.
//
// USAGE:
//---------------------------------------------------------------------------------------------
// - Set host serial terminal to 9600 baud
// - To open a I2C address (between 1 - 127), enter the number of the address - e.g. 99 ENTER
// - To issue a command, enter it directly to the console.
// - type "!help" to see available commands
//---------------------------------------------------------------------------------------------


#include <Wire.h>                        //enable I2C.
#include <errno.h>

enum errors {SUCCESS, FAIL, NOT_READY, NO_DATA };
enum states {REPL_READ_STATE, REPL_EVAL_STATE, REPL_PRINT_STATE, POLLING_READ_STATE};

char computer_data[20];                 // A 20 byte character array to hold incoming data from a pc/mac/other.
byte computer_bytes_count = 0;          // We need to know how many characters bytes have been received
int computer_in_byte;
boolean computer_msg_complete = false;

const int EZO_ANSWER_LENGTH = 32;
char ezo_answer[EZO_ANSWER_LENGTH];     // A 32 byte character array to hold incoming data from the sensors
int ezo_address = 0;                    // INT pointer for channel switching - 0-7 serial, 8-127 I2C addresses
String ezo_type;                        // hold the name / type of the EZO device
char* ezo_version;                      // hold the version of the EZO device

errors i2c_error;                       // error-byte to store result of Wire.transmissionEnd()
states state;
unsigned long next_receive_time;


void setup() {

  Serial.begin(9600);                    // Set the hardware serial port to 9600
  while (!Serial) ;                      // Leonardo-type arduinos need this to be able to write to the serial port in setup()
  Wire.begin();                          // enable I2C port.

  ezo_type.reserve(40);                  // reserve string buffer to save some SRAM

  intro();                               // display startup message
  state = REPL_READ_STATE;
}


void loop() {

  switch (state) {
    case REPL_READ_STATE:

      read_console();
      break;

    case REPL_EVAL_STATE:

      eval_command();
      break;

    case REPL_PRINT_STATE:

      receive_answer();
      break;

    case POLLING_READ_STATE:

      polling_read();
      break;
  }
}


// loop REPL_READ_STATE
void read_console() {
  while (Serial.available() > 0) {              // While there is serial data from the computer

    computer_in_byte = Serial.read();           // read a byte

    if (computer_in_byte == '\n' || computer_in_byte == '\r') {      // if a newline character arrives, we assume a complete command has been received
      computer_data[computer_bytes_count] = 0;
      computer_msg_complete = true;
      computer_bytes_count = 0;
    } else {                                    // command not complete yet, so just ad the byte to data array
      computer_data[computer_bytes_count] = computer_in_byte;
      computer_bytes_count++;
    }
  }

  if (computer_msg_complete) {                  // if there is a complete command from the computer
    state = REPL_EVAL_STATE;
    computer_msg_complete = false;              //Reset the var computer_bytes_count to equal 0
  }
}


// loop REPL_EVAL_STATE
void eval_command() {

  char *cmd = computer_data;
  Serial.print(ezo_address);
  Serial.print(F("> "));                       // echo to the serial console to see what we typed
  Serial.println(cmd);

  if (cmd[0] == 0 ) {                          // catch <ENTER>-only "command". not to be forwarded to the EZO device.
    state = REPL_READ_STATE;
    return;
  }
  else if (strcmp(cmd, "!help") == 0) {        // !help command
    help();
    computer_msg_complete = false;
    state = REPL_READ_STATE;
    return;
  }
  else if (strcmp(cmd, "!scan") == 0) {        // !scan command
    scan();
    computer_msg_complete = false;
    state = REPL_READ_STATE;
    return;
  }
  else if (strcmp(cmd, "!poll") == 0) {       // !poll command
    ezo_send_command("r");
    next_receive_time = millis() + 1000;
    state = POLLING_READ_STATE;
    return;
  }

  else {                                       // it's not a console command, so probaby it's a channel number or a command for the ezo device


    errno = 0;                                 // reset errno (errno.h) - it will be used by strtol below
    char *endptr = NULL;
    int _maybe_i2c_address = strtol(cmd, &endptr, 10); // strtol()
    
    // strtol sets endptr = input, if no digits are found to be converted
    if (cmd != endptr && errno == 0) {                  // check for errno - to distinguish between "0" input and \0 (error)

      if (_maybe_i2c_address >= 1 && _maybe_i2c_address <= 127) {
        if (set_active_ezo(_maybe_i2c_address)) {       // set I2C address

          serialPrintDivider();
          Serial.println( F("ACTIVE DEVICE: "));
          Serial.println( ezo_type );
          Serial.print( F("Address: ") );
          Serial.print( ezo_address );
          Serial.print( F(" | Firmware: "));
          Serial.println( ezo_version);
        }
        else {
          Serial.println(F("CHANNEL NOT AVAILABLE"));
          Serial.println(F("Try '!scan'."));
        }
        serialPrintDivider();
        state = REPL_READ_STATE;
        return;
        
      } else if (_maybe_i2c_address == 0) {           // don't try to set active ezo to 0 - set it back to "broadcast"
        ezo_address = 0;
        state = REPL_READ_STATE;
        return;
      }
    }

    // it's not a channel number. so let's forward this as a command to the EZO device.
    state = REPL_PRINT_STATE;

    if (strncmp(cmd, "Baud,", 5) == 0 || strncmp(cmd, "Serial,", 7) == 0) {
      Serial.println(F("! You have changed the EZO device to UART - it can no longer be accessed by this console. "));
    }

    ezo_send_command(cmd);

    if (cmd[0] == 'r' || cmd[0] == 'R' || strncmp ( cmd, "Cal", 3 ) == 0 || strncmp ( cmd, "cal", 3 ) == 0) { // it's a "R" or "Cal" command
      next_receive_time = millis() + 1000;
    } else {
      next_receive_time = millis() + 300;
    }
  }
}


// loop  REPL_PRINT_STATE
void receive_answer() {
  if (millis() > next_receive_time) {

    ezo_receive_command();

    Serial.print(ezo_address);
    Serial.print(F("> "));

    switch (i2c_error) {
      case SUCCESS:
        Serial.println(ezo_answer);
        break;

      case FAIL:
        Serial.println(F("COMMAND FAILED"));
        break;

      case NOT_READY:
        Serial.println(F("EZO DEVICE NOT READY"));
        break;

      case NO_DATA:
        Serial.println(F("NO DATA"));
        break;
    }

    state = REPL_READ_STATE;
  }
}


// loop POLLING_READ_STATE
void polling_read() {
  if (millis() > next_receive_time) {
    ezo_receive_command();
    if (i2c_error == 0) {
      Serial.print(ezo_address);
      Serial.print(F("> "));
      Serial.println(ezo_answer);
    }

    if (Serial.available() > 0) {
      state = REPL_READ_STATE;
    } else {
      ezo_send_command("r");
      next_receive_time = millis() + 1000;
    }
  }
}


//function controls which UART/I2C port is opened. returns true if channel could be changed.
boolean set_active_ezo(int _new_active_address) {
  ezo_address = _new_active_address;
  ezo_send_command("I");
  delay(400);
  ezo_receive_command();

  if (parseInfo()) {

    return true;
  }
  return false;
}


// parses the answer of the "i" command. returns true if answer was parseable, false if not.
boolean parseInfo() {

  char* _token = strtok(ezo_answer, ",");
  char* _type;

  if (_token != NULL) {

    _type = strtok(NULL, ",");
    //Serial.println(_type);

    ezo_version = strtok(NULL, ",");
    //Serial.println(ezo_version);

    if (strcmp(_type, "pH") == 0) {
      ezo_type = F("EZO pH Circuit");
    }
    else if (strcmp(_type, "ORP") == 0 || strcmp(_type, "OR") == 0) {
      ezo_type = F("EZO ORP Circuit");
    }
    else if (strcmp(_type, "DO") == 0) {
      ezo_type = F("EZO Dissolved Oxygen Circuit");
    }
    else if (strcmp(_type, "EC") == 0) {
      ezo_type = F("EZO Conductivity Circuit");
    }
    else if (strcmp(_type, "RTD") == 0) {
      ezo_type = F("EZO RTD Circuit");
    }
    else if (strcmp(_type, "FLO") == 0) {
      ezo_type = F("EZO FLOW - Embedded Flow Meter Totalizer");
    }
    else if (strcmp(_type, "CO2") == 0) {
      ezo_type = F("EZO CO2 - Embedded NDIR CO2 Sensor");
    }
    else if (strcmp(_type, "O2") == 0) {
      ezo_type = F("EZO O2 - Embedded Oxygen Sensor");
    }
    else if (strcmp(_type, "HUM") == 0) {
      ezo_type = F("EZO-HUM - Embedded Humidity sensor");
    }
    else if (strcmp(_type, "PRS") == 0) {
      ezo_type = F("EZO-PRS - Embedded Pressure Sensor");
    }
    else if (strcmp(_type, "PMP") == 0) {
      ezo_type = F("EZO-PMP - Embedded Dosing Pump");
    }
    else if (strcmp(_type, "RGB") == 0) {
      ezo_type = F("EZO-RGB - Embedded Color Sensor");
    }

    else {
      ezo_type = F("UNKNOWN EZO DEVICE");
    }

    return true;

  } else {

    // it's a legacy device (non-EZO) or something else (might be from the future)
    ezo_type = "UNKNOWN DEVICE";

    return false;                              // can not parse this info-string
  }
}


// scan the i2c bus for EZO devices
int scan() {

  int _ezo_address_before = ezo_address;       // remember active ezo device to return to after scanning
  int _ezo_count = 0;
  byte _i2c_error;
  int _address;

  serialPrintDivider();

  for (_address = 1; _address <= 127; _address++ ) {

    // "probing" the address - if result is '0', a device is present
    Wire.beginTransmission(_address);
    _i2c_error = Wire.endTransmission();

    if (_i2c_error == 0) {

      if (set_active_ezo(_address)) {

        _ezo_count++;
        Serial.print(  ezo_address);
        Serial.print(": ");
        Serial.println(  ezo_type);
      }
    }
  }

  if (_ezo_address_before == 0) {             // set the active ezo device back to what it was before the scan
    ezo_address = 0;
  } else {
    set_active_ezo(_ezo_address_before);
  }

  serialPrintDivider();

  Serial.print(    _ezo_count);
  Serial.println(  F(" EZO devices found"));

  return _ezo_count;
}


//print intro
void intro() {

  Serial.println( F("\n\n"));
  serialPrintDivider();
  Serial.println( F("Whitebox Interactive EZO Console"));

  if (scan() > 0) {
    Serial.println( F("\nType an I2C address to connect the console to an EZO deivce (1-127)"));
  }
  Serial.println( F("For info type '!help'"));
  Serial.println( F("\n"));
}


//print help dialogue
void help() {
  serialPrintDivider();
  Serial.println( F("To connect to an attached EZO device, type its address (1-127) followed by ENTER"));
  Serial.println( F("To send an I2C command to the EZO device, enter it directly to the console. e.g. 'r<ENTER>'"));
  Serial.println( F("You can find all available I2C commands in the datasheet of your EZO device."));
  Serial.println( F("Available console commands:"));
  Serial.println( F("!scan     lists all attached EZO devices"));
  Serial.println( F("!poll     polls the 'read' command of the current EZO device every second"));
  Serial.println( F("          to cancel polling, send any command or <ENTER>"));
  Serial.println( F("!help     this information"));
  serialPrintDivider();
}


void serialPrintDivider() {
  Serial.println(  F("------------------"));
}


// send a command to an EZO device
void ezo_send_command(const char* command) {
  Wire.beginTransmission(ezo_address);
  Wire.write(command);
  Wire.endTransmission();
}


// request answer from an EZO device
void ezo_receive_command() {
  byte sensor_bytes_received = 0;
  byte code = 0;
  byte in_char = 0;

  memset(ezo_answer, 0, EZO_ANSWER_LENGTH);                   // clear sensordata array;

  Wire.requestFrom(ezo_address, EZO_ANSWER_LENGTH - 1, 1);
  code = Wire.read();

  while (Wire.available()) {
    in_char = Wire.read();

    if (in_char == 0) {
      break;
    }
    else {
      ezo_answer[sensor_bytes_received] = in_char;
      sensor_bytes_received++;
    }
  }

  switch (code) {
    case 1:
      i2c_error = SUCCESS;
      break;

    case 2:
      i2c_error = FAIL;
      break;

    case 254:
      i2c_error = NOT_READY;
      break;

    case 255:
      i2c_error = NO_DATA;
      break;
  }
}

Testing sensors

  • Open the file using the Arduino IDE

  • Connect the Arduino Uno to your computer.
  • Ensure in the Arduino IDE that:
    • Under Tools > Board > Arduino Uno board is selected
    • Under Tools > Port - a suitable serial port is selected

  • Upload the Interactive EZO console by clicking on the upload button (arrow pointing to the right)

  • After uploading is complete click on the Serial monitor (button top right of screen)
  • Ensure the Serial monitor baud rate is set to 9600 bps.

  • The Serial Monitor should identify the number of devices connected to the Whitebox T1.
  • In this example three sensors have been identified and each sensor is assigned a number.
    • 97 - EZO Dissolved Oxygen Circuit
    • 100 - EZO Conductivity Circuit
    • 102 - EZO RTD Temperature Circuit

Interacting with EZO sensors

  • To connect to a device enter its channel number in the Serial Monitor.
  • For example, to interact with the Temperature sensor enter 102 and press Enter or click on the Send button.

  • The Serial Monitor will respond with a status report for the EZO device.

  • To take a temperature reading, enter the command r in the Serial Monitor and press Enter.

  • The temperature reading will now be output to the monitor.

Testing Atlas sensors with Whitebox T1 and Particle electron

Hardware Setup

  • Particle electron was powered using a 3.7V Li-Ion battery (2600 mAh)
  • The temperature, conductivity and dissolved oxygen sensors (3.3V or 5V devices) can be tested using the Whitebox T1 (3.3V or 5V device) and Particle electron (3.3V device).
  • Ensure that each EZO circuit has been changed to I2C mode. How to change data protocol of Atlas sensors
  • Ensure that the EC EZO (conductivity) circuit has been set up for the K0.1 probe (default setup is the K1.0 probe).
  • Connect the sensors as shown above.
  • Port connections
    • Port 1 - not connected
    • Port 2 - temperature sensor
    • Port 3 - conductivity sensor
    • Port 4 - dissolved oxygen sensor
  • Important notes:
    • Whiteboard T1 power mode is switched to Ardunio (not external).
    • The alignment of the EZO circuits and the sensor connector is slightly offset.
  • Port connections
    • Port 1 - not connected
    • Port 2 - temperature sensor
    • Port 3 - conductivity sensor
    • Port 4 - dissolved oxygen sensor
  • The Pololu 5V step-up voltage regulator can be easily damaged. During operation check that the output voltage is correct under load using a volt meter.

Pin connections Whitebox T1

  • SDA pin (Whitebox T1) to D0 pin Particle electron
  • SCL pin (Whitebox T1) to D1 pin Particle electron
  • GND pin (Whitebox T1) to GND pin Particle electron
  • IOREF pin (Whitebox T1) to VOUT pin Pololu 5V regulator

Pin connections Particle electron

  • D0 pin (Particle electron) connected to SDA pin (Whitebox T1)
  • D1 pin (Particle electron) connected to SCL pin (Whitebox T1)
  • D2 pin (Particle electron) connected to SHDN shutdown pin Pololu 5V voltage regulator U1V11F5
  • GND pin (Particle electron) connected to:
    • Whitebox T1
    • Pololu 5V voltage regulator
    • 10k ohm pull-down resistor - which is connected to SHDN pin on Pololu voltage regulator
  • A0 pin (Particle electron) connected to pressure (water level) sensor (pin not used in this example)
  • A1 pin (Particle electron) connected to turbidity sensor via voltage divider (pin not used in this example)

Pin connections Pololu 5V Step-Up Step-down Voltage Regulator S7V8F5

  • SHDN pin (Volt regulator) connected to D2 pin (Particle electron)
  • SHDN pin (Volt regulator) connected to GND via 10k ohm pull-down resistor
  • VIN pin (Volt regulator) connected to VB Battery pin (Particle electron)
  • GND pin (Volt regulator) connected to GND pin (Particle electron)
  • VOUT pin (Volt regulator) connected to IOREF pin (Whitebox T1)
  • VIN and GND pins (Volt regulator) bridged with 25V 100uF electrolytic capacitor

Pin connections for DHT22 internal temperature and humidity sensor

  • Pin 1 - Vcc - 5V supply (DHT22) connected to 5V regulated supply
  • Pin 2 - Data (DHT22) connected to D3 (Particle electron)
  • Pin 3 - not connected
  • Pin 4 - Ground (DHT22) connected to GND
    • Pin 1 and Pin 2 bridged with 10k ohm resistor (Pull-up)

Software Setup

Particle electron code has been collapsed by default. Click here to expand:

// 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;                   //used 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;  

float internal_Tvalue;
float internal_Hvalue;

//Sampling time 
int SampleTime = 30;        //Sample time in seconds
int SleepTime = 15;         //minutes to sleep before waking again 
int StartTime;  

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); 

//SystemSleepConfiguration config;


////////////////////////////////////////////////////////////////////////////////////////////////

void setup()                     //hardware initialization.
{
    
  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);

  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("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% : turning off power and going to sleep for 12 hours to charge battery");
        digitalWrite(PWR5V, LOW);
        System.sleep(SLEEP_MODE_DEEP, 12*60*60); 
        }
    else if (BatterySOC < double(40))
        {
        Serial.println("Battery below 40% : turning off power and going to sleep for 6 hours to charge battery");
        digitalWrite(PWR5V, LOW);
        System.sleep(SLEEP_MODE_DEEP, 6*60*60);   
        } 
    else if (BatterySOC < double(60))
        {
        Serial.println("Battery below 60% : turning off power and going to sleep for 2 hours to charge battery");
        digitalWrite(PWR5V, LOW);
        System.sleep(SLEEP_MODE_DEEP, 2*60*60); 
        }
    else
        {
        Serial.println(("Battery level OK"));
        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);
    
    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);
    
    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 Internal Temp: %f Internal Hum: %f Battery Voltage: %f Battery Charge: %f", Tvalue, ECvalue, DOvalue, NTUV, 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, 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)
  //System.sleep(SLEEP_MODE_DEEP, SleepTime * 60);
  
  // 60 second delay between readings
  delay(60000);
  
}

// 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 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("internal_T", internal_Tvalue);
   jw.insertKeyValue("internal_H", internal_Hvalue);
   jw.insertKeyValue("BatteryVoltage", BatteryVoltage);
   jw.insertKeyValue("BatterySOC", BatterySOC);
 }
 
 Particle.publish("PayloadJSON", jw.getBuffer(), PRIVATE);
}

Photos

  • Close up of Whitebox T1 and Particle electron

  • Side view perspective

Circuit schematic

Water Quality Sensor Assembly Whitebox T1 and Particle electron

Construction

  • Testing phase of Sensor Housing with temperature, conductivity, dissolved oxygen, turbidity, pressure and internal temperature, internal humidity sensors connected.


  • Close up of ProtoShield with Particle electron


Sensor housing

Header shield for Particle electron and Whitebox T1

  • Take 40 pin headers and Freetronics ProtoShield Basic for Arduino.
  • Cut header pins to the following lengths:
    • 8 pin length for POWER related pins on Arduino
    • 8 pin length for DATA pins D0 to D7 on the Arduino
    • 10 pin length for DATA pins D8 to D13 and SCL and SDA pins on the Arduino.
    • Note - no pin headers required for Analog pins A0 to A5.

  • Place header pins in Arduino Uno to hold in place.
  • Place ProtoSheld on top of Arduino Uno so that header pins are correctly located within shield.
  • Solder pins in place.
  • The only pins that are used by the Whitebox T1 are the following.
    • IOREF - 5V power supply for the Whitebox T1
    • GND - there are three ground pins
    • SDA - data communication line for I2C
    • SCL - clock communication line for I2C
    • all other pins are only used to physically support the ProtoShield when plugged into the Whitebox T1

Particle electron header pins
  • Cut female header pins to 12 pin length (x2).
  • Place on ProtoShield board as shown.

  • Note the gap between the GND rail on the ProtoShield and the female header pins as shown below.

  • Note that the gap is different for the 5V rail on the ProtoShield.
  • Also note that the Particle electron will over-hang to save space on the ProtoShield and because not all the pins on the Particle electron are used.
  • When soldering keep the Particle electron in place to help the header pins align correctly.

  • Label the header pins close to the 5V rail on the ProtoShield to help prevent errors when wiring up the board.
    • VIN - Voltage IN - the solar panel connects to the VIN and GND pins.
    • GND - ground for the solar panel input.
    • A0 - Analog 0 (zero) input pin - receiving input from the pressure sensor
    • A1 - Analog 1 input pin - receiving input from the turbidity sensor via a voltage divider.

  • Label the header pins close to the GND rail on the ProtoShield.
    • D0 - Digital pin 0 (zero) - connects to SDA (I2C data communication)
    • D1 - Digial pin 1 - connects to SCL (I2C clock communication)
    • D2 - Digital pin 2 - connects to voltage regulator shutdown (SHDN) pin. Digial pin set high turns 5V regulator ON.
    • D3 - Digial pin 3 - connects to the DHT22 temperature and humidity data line.

Voltage regulator header pins
  • Solder a 4 pin female header to the ProtoShield to accept the Pololu 5V Step-Up/Step-Down Voltage Regulator S7V8F5.
  • Solder the regulator as shown.
  • Use a ball of blue-tack to hold the header pin in place.
  • Note that labels are added to the header pin.
    • SH - SHDN - shutdown
    • VI - VIN - Voltage In
    • GND - ground
    • VO - VOUT - Voltage Out

  • Four pin female header location shown without 5V regulator.

DHT22 header pins
  • Solder a 4 pin female header to the ProtoShield to accept the DHT22 temperature and humidity sensor.

  • Solder the DHT22 temperature sensor to a 4 pin male header.
  • Label both the female header pin and the DHT22 sensor.
    • pin1 (+) Positive 5V supply
    • pin 2 (D) data line
    • pin 3 not used
    • pin 4 (-) ground or negative terminal

Turbidity sensor header pins
  • Solder a 3 pin made header to the ProtoShield to accept the turbidity sensor female plug.

  • Top down view of ProtoShield.
  • Pin labels are:
    • pin 1 (-) ground
    • pin 2 (+) 5V supply
    • pin 3 (S) data analog signal form 0 to 5V

Solar power and Pressures sensor Screw Terminal blocks
  • Solder
    • a 3-pin screw terminal block for the Pressure sensor. Terminal placed in outer analog pins A1, A3 and A5.
    • a 2-pin screw terminal block for the Solar Panel supply. Terminal placed in inner digital pins D9 and D11.
  • Note that these specific analog and digital pins on the ProtoShield are not used by the Whitebox T1 or connected to the Particle electron.

  • Aerial view of ProtoShield showing the solar panel and pressure sensor connections

ProtoSheld wiring - negative wires
  • Solder:
    • all negative wire connections
    • pull-down resistor (10 k ohms) for the 5V regulator
    • two 10 k ohm resistors that form a voltage divider circuit for the analog output of the turbidity sensor

  • Note that the two 10k ohm resistors that form the voltage divider circuit are soldered together on the top of the board.

ProtoSheld wiring - positive wires
  • Solder all positive wire connections.


ProtoSheld wiring - data and digiatal pin wires
  • Solder:
    • D0 pin (Particle electron) to SDA (Whitebox T1 shield) - Green wire
    • D1 pin (Particle electron) to SCL (Whitebox T1 shield) - Glue wire
    • D2 pin (Particle electron) to SHDN pin (Voltage regulator) - White wire
    • D3 pin (Particle electron) to DHT22 Data pin (DHT 22 temperature sensor) - White wire


ProtoSheld wiring - analog pin wires
  • Solder:
    • A0 analog pin (Particle electron) to output of Pressure sensor - Yellow wire
    • A1 analog pin (Particle electronc) to voltage divider connected to output turbidity sensor - Yellow wire

ProtoSheld wiring - capacitor and resistor
  • Solder:
    • Capacitor (25V 100uF) across the GND and VIN pins on the 5V voltage regulator
    • Resistor (10 k ohms) across the VIN and Data pins on the DHT22 temperature sensor

Assembly of sensor housing
Testing of all components
  • All sensors are connected so that the unit can be fully tested before being deployed into the field.
  • Conductivity and Dissolved Oxygen sensors need to be calibrated.
  • The turbidity sensor should be check that it has a 4.5-5V output in clear water, and a voltage approaching zero in turbid water. The turbidity sensor feeds into an analog pin on the Particle electron and will produce values ranging from 0 (0V) to 4095 (3.3V). The turbidity sensor output is halved using a voltage divider circuit.
  • The pressure sensor also produces a voltage output from 0 to 5V. The base voltage during normal operation is 0.45V. It does not pass through a voltage divider circuit. The greatest voltage output will be 5V at 10Bar (10m depth).


Code test for PS-1 and PS-2 18 March 2022

Particle electron code for PS-1 and PS-2 on 18 March 2022 has been collapsed by default. Click here to expand:

// 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;


////////////////////////////////////////////////////////////////////////////////////////////////

void setup()                     //hardware initialization.
{
    
  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);

  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)
  System.sleep(SLEEP_MODE_DEEP, (SleepTime * 60)); // wake up every 15 minutes
  
  // 60 second delay between readings
  //delay(60000);
  
}

// 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);
}

Integrations and Web Hooks

  • Web hooks send data received by the Particle server to a destination server.
  • Web hooks also arrange data with appropriate labels and may format the data using JSON format to make it more readable.
  • In this example, Webhooks were created for environmental sensors PS-1 and PS-2

Web hook code for PS-1 and PS-2 on 18 March 2022 has been collapsed by default. Click here to expand:

{
  "docType": "jts",
  "version": "1.0",
  "header": {
    "columns": {
      "0": {
        "series": "Timestamp",
        "name": "DateTime",
        "dataType": "NUMBER",
        "units": ""
      },
      
      "1": {
        "series": "Temperature",
        "name": "T",
        "dataType": "NUMBER",
        "units": "°C"
      },
      
      "2": {
        "series": "Conductivity",
        "name": "EC",
        "dataType": "NUMBER",
        "units": "uS/cm"
      },
      
      "3": {
        "series": "Dissolved Oxygen",
        "name": "DO",
        "dataType": "NUMBER",
        "units": "mg/L"
      },
      
      "4": {
        "series": "Raw Turbidity",
        "name": "NTUV",
        "dataType": "NUMBER",
        "units": "V"
      },
      
       "5": {
        "series": "Pressure",
        "name": "pressure",
        "dataType": "NUMBER",
        "units": "V"
      },     
      
      
      "6": {
        "series": "Internal Temperature",
        "name": "internal_T",
        "dataType": "NUMBER",
        "units": "°C"
      },
      
      "7": {
        "series": "Internal Humidity",
        "name": "internal_H",
        "dataType": "NUMBER",
        "units": "%"
      },
     
      "8": {
        "series": "Battery Voltage",
        "name": "BatteryVoltage",
        "dataType": "NUMBER",
        "units": "V"
      },
      
      "9": {
        "series": "Battery Level",
        "name": "BatterySOC",
        "dataType": "NUMBER",
        "units": "%"
      }
    }
  },
  "data": [
    {
      "ts": "{{{PARTICLE_PUBLISHED_AT}}}",
      "f": {
        "0": {
          "v": {{{DateTime}}}
        },
        
        "1": {
          "v": {{{T}}}
        },
        
        "2": {
          "v": {{{EC}}}
        },
        
        "3": {
          "v": {{{DO}}}
        },
                
        "4": {
          "v": {{{NTUV}}}
        },
  
        "5": {
          "v": {{{pressure}}}
        },
        
        "6": {
          "v": {{{internal_T}}}
        },
        
        "7": {
          "v": {{{internal_H}}}
        },
        
        "8": {
          "v": {{{BatteryVoltage}}}
        },
        
        "9": {
          "v": {{{BatterySOC}}}
        }
      }
    }
  ]
}

Verifying data received in Bio2Lab Eagle IO

  • The most currently data received is presented in the Bio2Lab Eagle IO Dashboard.