James Yu
Published © GPL3+

An Urban Garden Monitor

An innovative solution for remote monitoring of data and trends in an urban garden environment.

IntermediateFull instructions provided3 hours4,680
An Urban Garden Monitor

Things used in this project

Hardware components

WIZ750SR-TTL-EVB Kit
WIZnet WIZ750SR-TTL-EVB Kit
Only the Serial to Ethernet module is used in the final product. The other equipment is used for testing and configuration of the board.
×1
Arduino 101
Arduino 101
I used the Arduino 101, which is identical to the Genuino 101.
×1
Adafruit BMP180 Barometric Pressure/Temperature/Altitude Sensor- 5V ready
This module is retired; I used it because I had it available. The successor, the BMP280 unit, has a different API which will require some adaptation to use.
×1
DHT22 Temperature Sensor
DHT22 Temperature Sensor
Compared to the DHT11, this unit offers greater temperature and humidity range, which is beneficial for the diverse climates around the world.
×1
Holder for Arduino
I used a clear transparent holder that came with another board. The Arduino 101 did not come with a holder, but you can obtain one from the above link. This holder is useful for protecting the board when in use.
×1
SparkFun Solid Core Jumper Wires, 22AWG
All the jumper wires you need can be found in this kit. See the circuit diagram for sizes and quantities.
×1
SparkFun Breadboard - Mini Modular (White)
×1
USB-A to B Cable
USB-A to B Cable
Make sure your cable is long enough to reach from your wall outlet to your application area. If not, consider also using a USB A extension cable.
×1
USB Wall Charger
These are the devices you plug into the wall to charge mobile devices. Any generic USB wall charger will work. I used a simple 5V 1A charger with a single USB A output.
×1
RJ45 Ethernet Cable
The direct type, not the crossover type. Make sure your cable can reach from your router to your device's application area.
×1
USB to RS232 DB9 Serial Adapter
If your computer does not have a DB9 port, you will need this component to test the WIZ750SR-TTL-EVB. Make sure that the DB9 end is able to plug into the serial cable included with the EVB kit.
×1
SparkFun Jumper Wires - Connected 6" (F/F, 20 pack)
I used four of these wires in the Urban Garden Monitor.
×1

Software apps and online services

Arduino IDE
Arduino IDE
SocketTest - Test My Socket
Easy to use TCP Client/Server software used for communication with the WIZ750SR over Ethernet.
WIZnet S2E Configuration Tool
WIZnet S2E Configuration Tool
Configuration tool for the WIZ750SR.

Hand tools and fabrication machines

Electrical Tape
Generic electrical tape used to secure everything together. A pair of scissors helps with this.

Story

Read more

Schematics

An Urban Garden Monitor Circuit Schematic

Circuit diagrams for the Urban Garden Monitor. Please note the comments that will help you during the process of creating the device.

Code

An Urban Garden Monitor Program

Arduino
This code runs the Urban Garden Monitor. Make sure to upload it to your device after you have installed all additional libraries as well as the Intel Curie boards core.
/*
   This is a sketch for the Urban Garden Monitor.

   It employs the Arduino 101 to collect temperature, pressue & humidity variables, and sends them using a WIZ750SR over Ethernet to a TCP Client terminal where data can be read.

   The onboard Pattern Matching Engine classifies daily averages of the data into a 30-category bank, creating an archive of data trends that can be interpreted by the user.

   It also saves all categorizations of the past 30 days for visualization of trends by the user.

   Make sure that you have the Adafruit sensor libraries and the CuriePME library installed.
*/

// Sensor libraries called in the code:

#include <Adafruit_Sensor.h> // Support library for the sensors.

// DHT sensor library, works with DHT11 or DHT22.
#include <DHT.h>
#include <DHT_U.h>

// This function declares an instance of the DHT sensor by stating the sensor readings pin, in this case pin 12, as well as the sensor type.
DHT_Unified dht(12, DHT22);
// I2C communication library.
#include <Wire.h>
// BMP085 Library, which also works with the BMP180 used in the device.
#include <Adafruit_BMP085_U.h>
// This function declares an instance of the BMP180.
Adafruit_BMP085_Unified BMP180 = Adafruit_BMP085_Unified(18001);

// Arduino 101 specific libraries:
#include <CurieTime.h> // Real-time clock.
#include <CuriePME.h> // Pattern-matching engine library.
#include <CurieTimerOne.h> // Timer functions library.

void setup() {
  // The code in setup() prepares all the components of the device and allows the user to set the current time.

  String time1, time2;
  // Initialize sensors, the PME and the serial port.
  BMP180.begin();
  dht.begin();
  CuriePME.begin();
  Serial1.begin(115200);
  while (!Serial1);
  // Do nothing while waiting for serial input.
  while (Serial1.available() == 0)
  {

  }
  // Read the input to clear the serial buffer but do nothing with the data.
  Serial1.readString();
  Serial1.println("UGM V.1.0.0 RELEASE");
  Serial1.println("By James Yu");
  Serial1.println();
  Serial1.print("Enter the current 24h time (hours): ");
  while (Serial1.available() == 0)
  {

  }
  // Wait for data to be typed in and save the data when it is.
  time1 = Serial1.readString();
  Serial1.println(time1);
  Serial1.print("Enter the current 24h time (minutes): ");
  while (Serial1.available() == 0)
  {

  }
  // Wait for data and save it.
  time2 = Serial1.readString();
  Serial1.println(time2);
  Serial1.println(time1 + ":" + time2);

  // This function sets the Arduino 101 RTC to the speficied times. As only hours and minutes are relevant, everything else is pre-defined.
  setTime(time1.toInt(), time2.toInt(), 00, 01, 01, 2018);
  Serial1.println();
  Serial1.println("LIST OF COMMANDS");
  Serial1.println("     LD Loop sensor data every 2 seconds.");
  Serial1.println("     DL Disable sensor data loop.");
  Serial1.println("     DD Display sensor data.");
  Serial1.println("     SS Show statistics of previous day's data readings.");
  Serial1.println("     ST Show trends of data in the past 30 days.");
  Serial1.println("     DC Display characteristics save data.");
  Serial1.println("     SC Show command list.");
}

// Boolean variables that tell whether a sensor is actually active or not.
boolean humidityStatus, temperatureStatus, bmpStatus;
// Numerical data variables.
float humidityValue, temperatureValue, bmpTempValue, pressureValue, temperature;

float data[24][3]; // Array for hourly data.
float vectorF[3]; // Array for averages of hourly data.
uint8_t vector[4]; // Array for averages to be classified into the PME.
float characteristics[30][3]; // Array for PME classifications and associated data.
int dayData[30]; // Array for classifications of the past 30 days.

int i;
int j = 0;
int category = 0;
int prevDayResponse = -1;

void loop() {
  // The code in the first section of loop() interprets serial commands.

  // If serial data is available during the loop, read it.
  if (Serial1.available() > 0)
  {
    String commandX = Serial1.readString();
    // Having "\r\n" at the end of each string check is necessary for the Arduino 101 to interpret sent strings properly.
    if (commandX == "LD\r\n")
    {
      CurieTimerOne.start(2000000, &dataLoopISR); // Activates an interrupt service routine every 2 seconds, which is set alongside any other code running.
    }
    else if (commandX == "DL\r\n")
    {
      CurieTimerOne.kill(); // Disables the interrupt service routine.
    }
    else if (commandX == "DD\r\n")
    {
      // This function prints out a single instance of sensor readings at the time the command is sent.
      Serial1.print("Time:");
      Serial1print(hour());
      Serial1.print(":");
      Serial1print(minute());
      Serial1.println();
      // If both the BMP180 and DHT22 are available, use the average of the two temperature values as the recorded temperature value.
      if (bmpStatus == true && temperatureStatus == true)
      {
        temperature = (bmpTempValue + temperatureValue) / 2;
        Serial1.println("Temperature:" + String(temperature) + "*C"); // Prints temperature.
        Serial1.println("Pressure:" + String(pressureValue) + "hPa"); // Prints pressure.
      }
      // If only the BMP180 is available for temperature, use that.
      else if (bmpStatus == true && temperatureStatus == false)
      {
        temperature = bmpTempValue;
        Serial1.println("Temperature:" + String(temperature) + "*C");
        Serial1.println("Pressure:" + String(pressureValue) + "hPa");
      }
      // If only the DHT22 is available, use that.
      else if (temperatureStatus == true && bmpStatus == false)
      {
        temperature = temperatureStatus;
        Serial1.println("Temperature:" + String(temperature) + "*C");
      }
      // If the DHT22 is available, read humidity values.
      if (humidityStatus == true)
      {
        Serial1.println("Humidity:" + String(humidityValue) + "%"); // Prints humidity.
      }
    }
    else if (commandX == "SS\r\n")
    {
      // This command prints data averages and classification of the previous day's readings.
      if (prevDayResponse == -1)
      {
        // This will only be called if no data has been classified yet. After the first 24 hours of operation, this should not appear again.
        Serial1.println("No previous day data yet!");
      }
      else
      {
        // If data exists, print it out.
        Serial1.println("Yesterday's data recordings:");
        Serial1.println("     Recorded category: " + String(prevDayResponse));
        Serial1.println("     Category temperature: " + String(characteristics[prevDayResponse][0]) + "*C"); // Data of the previous day's categorization (out of the 30 possible categories).
        Serial1.println("     Category pressure: " + String(characteristics[prevDayResponse][1]) + "hPa");
        Serial1.println("     Category humidity: " + String(characteristics[prevDayResponse][2]) + "%");
        Serial1.println("     Actual temperature: " + String(vectorF[0]) + "*C"); // The previous day's actual data for comparison.
        Serial1.println("     Actual pressure: " + String(vectorF[1]) + "hPa");
        Serial1.println("     Actual humidity: " + String(vectorF[2]) + "%");
      }
    }
    else if (commandX == "ST\r\n")
    {
      // This command calls up all the elements of the categorizations of the past 30 days.
      Serial1.println("Environment data for the past 30 available days:");
      Serial1.println("");
      for (int s = 0; s < 30; s++) // Print the array's elements sequentially.
      {
        Serial1.println("     " + String(dayData[s]));
      }
    }
    else if (commandX == "DC\r\n")
    {
      // This command prints out all the data associated with each category saved in the PME. This is helpful when used with the previous command.
      Serial1.println("Listing all saved daydata characterizations:");
      for (int h = 0; h < 30; h++)
      {
        // For each of the 30 categories, print associated temperature, pressure and humidity variables.
        Serial1.println();
        Serial1.println("     Category: " + String(h));
        Serial1.println("     Temperature: " + String(characteristics[h][0]) + "*C");
        Serial1.println("     Pressure: " + String(characteristics[h][1]) + "hPa");
        Serial1.println("     Humidity: " + String(characteristics[h][2]) + "%");
      }
    }
    else if (commandX == "SC\r\n")
    {
      // This prints the commands list.
      Serial1.println("LIST OF COMMANDS");
      Serial1.println("     LD Loop sensor data every 2 seconds.");
      Serial1.println("     DL Disable sensor data loop.");
      Serial1.println("     DD Display sensor data.");
      Serial1.println("     SS Show statistics of previous day's data readings.");
      Serial1.println("     ST Show trends of data in the past 30 days.");
      Serial1.println("     DC Display characteristics save data.");
      Serial1.println("     SC Show command list.");
    }
  }

  // The code in this section of loop() runs continuously and polls the sensors for data.

  // Define a sensor reading event for the BMP180.
  sensors_event_t event;
  BMP180.getEvent(&event); // Obtain sensor readings.
  if (event.pressure) // If air pressure data is available, the BMP180 is active (true). Obtain pressure and temperature data.
  {
    bmpStatus = true;
    pressureValue = event.pressure;
    BMP180.getTemperature(&bmpTempValue);
  }
  else // If no data is available, set the BMP180's status to unavailable (false). This should only happen if the sensor is not wired properly.
  {
    bmpStatus = false;
  }

  // Define a sensor reading event for the DHT22.
  sensors_event_t event1;
  dht.temperature().getEvent(&event1); // Get readings.
  if (isnan(event1.temperature)) {
    // If there are no numerical readings from the sensor, set the device's status for this variable to unavailable (false).
    temperatureStatus = false;
  }
  else {
    // If data is available, set status to true and get temperature data.
    temperatureStatus = true;
    temperatureValue = event1.temperature;
  }
  dht.humidity().getEvent(&event1);
  if (isnan(event1.relative_humidity)) {
    // If there are no numerical readings from the sensor, set the device's status for this variable to unavailable (false).
    humidityStatus = false;
  }
  else {
    // If data is available, set status to true and get humidity data.
    humidityStatus = true;
    humidityValue = event1.relative_humidity;
  }

  // This section of loop() runs every hour. It loads the environment data polled when the function runs into an array to be interpreted later.

  if (minute() == 0 && second() < 8 && bmpStatus == true && temperatureStatus == true && humidityStatus == true)
    // This only runs if all sensors are active. It has a 7 second buffer to prevent being missed if another function is running at the same time.
  {
    Serial1.println("Recording data...");
    // Put data into the spots in the array dedicated to the current hour.
    data[hour()][0] = temperature;
    data[hour()][1] = pressureValue;
    data[hour()][2] = humidityValue;
    if (j < 24) // If 24 hours have not elapsed, increment the hour counter.
    {
      j++;
    }
    else
    {
      // If 24 hours have passed, take an average of the data and load it into the PME.
      for (int x = 0; x < 24; x++)
      {
        // Conglomerate the sums of each variable type into a single array spot.
        vectorF[0] += data[x][0];
        vectorF[1] += data[x][1];
        vectorF[2] += data[x][2];
      }
      // Divide each value by 24, giving the average of the day's data.
      vectorF[0] = vectorF[0] / 24;
      vectorF[1] = vectorF[1] / 24;
      vectorF[2] = vectorF[2] / 24;

      // Convert the data to 8-bit format to be used for day categorization and comparison by the PME.
      vector[0] = map(constrain((int)vectorF[0], -40, 40), -40, 40, 0, 80); // Handles temperature data.

      // As pressure data will not fit in an 8-bit variable, split it in half.
      vector[1] = (int)(vectorF[1] / 100); // Takes the first 2 digits by dividing the value by 100 and converting to an integer, removing all digits after the decimal point.
      vector[2] = (int)vectorF[1] - (((int)(vectorF[1] / 100)) * 100); // Takes the last 2 digits by subtracting the above value from the original value * 100 from the original value.

      vector[3] = constrain((int)vectorF[2], 0, 255); // Handles humidity data.
      // Attempt to classify the data into the PME and get a response.
      int response = CuriePME.classify(vector, 4);
      if (response == CuriePME.noMatch)
      {
        // If the PME cannot classify the data, create a new category and use the data to characterize it.
        Serial1.println("New characterization-saving to memory.");
        CuriePME.learn(vector, 4, category);
        characteristics[category][0] = vectorF[0];
        characteristics[category][1] = vectorF[1];
        characteristics[category][2] = vectorF[2];
        prevDayResponse = category; // Set the "previous day variable" category to this so it can be called upon by the check previous day's data function later.
        category++; // Increase the category count.
        if (category == 30)
        {
          // If the amount of categories has exceeded 30, reset the category at 0 and keep going. This will allow the PME to progress by deleting the oldest category's data so as to not run out of space.
          category = 0;
        }
      }
      else
      {
        // If the PME found a category that closely matches the data of the past 24 hours, state it.
        Serial1.println("Data of the past 24H is similar to days with the following characteristics:");
        Serial1.println("     Category: " + String(response));
        Serial1.println("     Temperature avg: " + String(characteristics[response][0]) + " *C");
        Serial1.println("     Pressure avg: " + String(characteristics[response][1]) + " hPa");
        Serial1.println("     Humidity avg: " + String(characteristics[response][2]) + " % ");
        Serial1.println();
        Serial1.println("The actual data of the past 24H:");
        Serial1.println("     Temperature avg: " + String(vectorF[0]) + " *C");
        Serial1.println("     Pressure avg: " + String(vectorF[1]) + " hPa");
        Serial1.println("     Humidity avg: " + String(vectorF[2]) + " % ");
        prevDayResponse = response;
      }
      for (int q = 29; q > 0; q--)
      {
        // This function handles the 30 day characterization component. To prevent this array from overloading, the program moves each component of the array forward one unit.
        // This causes the oldest data, hosted on the last spot, to be deleted.
        // This also opens up the first spot to accepting new data.
        dayData[q] = dayData[q - 1];
      }
      dayData[0] = prevDayResponse; // Save the day's categorization to the array.
    }
    delay(8000); // Wait 8 seconds to make sure this function doesn't accidentally run twice in the same hour.
    Serial1.println("Done.");
  }
  // End of loop code, return to the beginning of the loop.
}

// This function helps with printing the time. If either the hours or minutes variable is less than 10, it adds a zero in front to make it fit ##:## format.
void Serial1print(int number) {
  if (number >= 0 && number < 10)
    Serial1.print('0'); {
  }
  Serial1.print(number);
}

// This function is an interrupt service routine called by the program when the "loop data" command is activated. It prints the same data as the "display data" command but loops every 2 seconds.
void dataLoopISR()
{
  // See the display data command for info on the components of this function.
  Serial1.print("Time: ");
  Serial1print(hour());
  Serial1.print(":");
  Serial1print(minute());
  Serial1.println();
  if (bmpStatus == true && temperatureStatus == true)
  {
    temperature = (bmpTempValue + temperatureValue) / 2;
    Serial1.println("Temperature: " + String(temperature) + "*C");
    Serial1.println("Pressure: " + String(pressureValue) + "hPa");
  }
  else if (bmpStatus == true && temperatureStatus == false)
  {
    temperature = bmpTempValue;
    Serial1.println("Temperature: " + String(temperature) + "*C");
    Serial1.println("Pressure: " + String(pressureValue) + "hPa");
  }
  else if (temperatureStatus == true && bmpStatus == false)
  {
    temperature = temperatureStatus;
    Serial1.println("Temperature: " + String(temperature) + "*C");
  }
  if (humidityStatus == true)
  {
    Serial1.println("Humidity: " + String(humidityValue) + " % ");
  }
  // Once everything has been printed out, restart the ISR timer and set it to wait for 2 seconds before running this function again. This allows it to loop.
  CurieTimerOne.restart(2000000);
}

Credits

James Yu

James Yu

2 projects • 13 followers
Hobbyist fascinated with technology and Arduino, currently studying at UBC.

Comments