Project showcase
Smart Terrarium

Smart Terrarium © GPL3+

Automated environment for reptiles to promote good husbandry and enable maximum lifespan for animals in captivity.

  • 998 views
  • 0 comments
  • 8 respects

Components and supplies

Necessary tools and machines

3drag
3D Printer (generic)

About this project

Intro

Smart Terrarium is the all in one system that provides the necessary information and automation to encourage a full and happy life for your reptiles. This system provides the user with the ability to monitor a variety of sensors in their animals habitat as well as control the lighting and misting system.

Backstory

This system was designed for my chameleon, Curie. She is a veiled chameleon and loves having her cage always kept in the optimal conditions. She is currently benefiting from the daily scheduled misting routines that the Smart Terrarium provides. This keeps her humidity in the appropriate range while also providing her with a nice place to cool off and get a drink.

Another great part of having the Smart Terrarium is peace of mind. You no longer need to randomly check on the status of the mister bucket water level. You can always rely on the Smart Terrarium to notify you when you are running low. Also get notifications when the drain bucket is approaching maximum capacity.

The most important part of this setup is the monitoring of the UV light. Chameleons require UV-B to produce Vitamin D. When a chameleon does not absorb enough calcium a chameleon can develop abnormalities in bone growth, rachitis, Metabolic Bone Disease (MBD) or just get very weak bones that are prone to breaking. The Smart Terrarium provides constant monitoring of UV light and can notify the user when the average UV index is below a certain threshold.

Hardware

This project uses a Raspberry Pi 3, an Arduino Uno, and a Arduino MKR1000. The Raspberry Pi acts as our main system receiving readings from the Uno and MKR1000. The Pi also acts as a web server which delivers our app to the client when a user wants to monitor their system. They simply log in to a web page allowing them to interact with the Smart Terrarium from anywhere around the globe.

The MKR1000 is the main workhorse for getting sensor data and it sends its readings every second over wifi to the web server running on the Raspberry Pi. It has the following sensors connected to it:

  • (5) DS18B20 Digital Tempearture probes
  • (1) DHT22 Temperature & Humidity sensor
  • (1) UV sensor
  • (1) Soil Moisture sensor

The Arduino Uno is connected to the Raspberry Pi via USB. It is used to monitor the water levels and also control the relays for the lights and misting pump. It has the following hardware connected:

  • (2) HC-SR04 Ultrasonic Sensor
  • (2) Relays

Software

This system leverages the following open source software:

  • Node.js
  • Johnny-five
  • RethinkDB
  • Firmata
  • Vue

Backend

The Raspberry Pi is running an Express web server which collects readings from the sensors and saves them to a RethinkDB instance. The readings are being reported to the Pi every second so the user has access to the most current measurements. The readings are saved every 5 minutes in RethinkDB for historical data.

The Uno is running the PingFirmataSketch and using serial to expose the proximity sensors and relays to the Pi using Firmata protocol. The Pi leverages Johnny-five to allow the user to program in javascript.

The MKR1000 is running a custom sketch using OneWire for the 5 temperature probes and then using analog inputs for the other sensors. It posts the values to the Pi every second making sure the readings the user sees are fresh.

Frontend

The Raspberry Pi also serves up the front end portion of this app which leverages Vue.js. It allows the user to log in from anywhere having complete control over their pet's environment. The app works on any modern web browser so you can access it from a computer, tablet, or phone.

The Smart Terrarium app allows the user to view the latests readings along with a sparkline graph of the previous day's historical data.

The user can click through on any of the sensors to view their history data. The Measurements page shows a graph with the entire sensor history. The user can select a section of the bottom graph to display a zoomed in version in the graph above. This allows the user to inspect the historic data for any spikes or dips where the environment is out of the norm.

The user has the ability to schedule events including lighting and misting events. The Event page displays the currently scheduled events and allows the user to add, edit, and delete events. These events are used to automate the lighting and misting sessions which provides peace making sure you never forget.

The Controls page allows the user to manually override the scheduled lighting and misting events. The page displays the current status of each relay and allows the user to toggle them with a click of a button.

The Admin page allow the user to enter information about themselves for contact purposes. This is for the email notifications on high/low water levels on misting system as well as notifications for when the daily average UV index gets too low. There is also a spot for the animals birthday so we can provide birthday notifications.

Screenshots

Home Page

Historical Measurements by type.

Events page for lighting and misting events.

Controls page where the user can override the scheduled events.

Demo

Here is a demo of the overall system.

Demo of The Smart Terrarium

We hope you enjoyed our presentation of the Smart Terrarium and encourage any reptile owners to use this project to improve the lives of your pets.

Thanks for your time and happy making!

Code

Ping FirmataC#
This is the version of firmata required for the Proximity sensors to function properly. Install this sketch on the Arduino Uno.
/*
 * Firmata is a generic protocol for communicating with microcontrollers
 * from software on a host computer. It is intended to work with
 * any host computer software package.
 *
 * To download a host software package, please clink on the following link
 * to open the download page in your default browser.
 *
 * http://firmata.org/wiki/Download
 */

/*
  Copyright (C) 2006-2008 Hans-Christoph Steiner.  All rights reserved.
  Copyright (C) 2010-2011 Paul Stoffregen.  All rights reserved.
  Copyright (C) 2009 Shigeru Kobayashi.  All rights reserved.
  Copyright (C) 2009-2011 Jeff Hoefs.  All rights reserved.
  Copyright (C) 2012 Julian Gaultier.  All rights reserved.
  Copyright (C) 2015 Rick Waldron.  All rights reserved.

  This library is free software; you can redistribute it and/or
  modify it under the terms of the GNU Lesser General Public
  License as published by the Free Software Foundation; either
  version 2.1 of the License, or (at your option) any later version.
*/
#include <Servo.h>
#include <Wire.h>
#include <Firmata.h>

#define I2C_WRITE B00000000
#define I2C_READ B00001000
#define I2C_READ_CONTINUOUSLY B00010000
#define I2C_STOP_READING B00011000
#define I2C_READ_WRITE_MODE_MASK B00011000
#define I2C_10BIT_ADDRESS_MODE_MASK B00100000

#define MAX_QUERIES 8
#define MINIMUM_SAMPLING_INTERVAL 10

#define REGISTER_NOT_SPECIFIED -1
#define PING_READ 0x75
// PING_READ is for use with HCSR04 and similar "ultrasonic ping" components

/*==============================================================================
 * GLOBAL VARIABLES
 *============================================================================*/

// analog inputs
int analogInputsToReport = 0; // bitwise array to store pin reporting

// digital input ports
byte reportPINs[TOTAL_PORTS];   // 1 = report this port, 0 = silence
byte previousPINs[TOTAL_PORTS]; // previous 8 bits sent

// pins configuration
byte pinConfig[TOTAL_PINS];         // configuration of every pin
byte portConfigInputs[TOTAL_PORTS]; // each bit: 1 = pin in INPUT, 0 = anything else
int pinState[TOTAL_PINS];           // any value that has been written

// timer variables
unsigned long currentMillis;  // store the current value from millis()
unsigned long previousMillis; // for comparison with currentMillis
int samplingInterval = 19;    // how often to run the main loop (in ms)

// i2c data
struct i2c_device_info
{
  byte addr;
  byte reg;
  byte bytes;
};

// for i2c read continuous more
i2c_device_info query[MAX_QUERIES];

byte i2cRxData[32];
boolean isI2CEnabled = false;
signed char queryIndex = -1;

// default delay time between i2c read request and Wire.requestFrom()
unsigned int i2cReadDelayTime = 0;

Servo servos[MAX_SERVOS];
/*==============================================================================
 * FUNCTIONS
 *============================================================================*/

void readAndReportData(byte address, int theRegister, byte numBytes)
{
  // allow I2C requests that don't require a register read
  // for example, some devices using an interrupt pin to signify new data available
  // do not always require the register read so upon interrupt you call Wire.requestFrom()
  if (theRegister != REGISTER_NOT_SPECIFIED)
  {
    Wire.beginTransmission(address);
    Wire.write((byte)theRegister);
    Wire.endTransmission();
    delayMicroseconds(i2cReadDelayTime); // delay is necessary for some devices such as WiiNunchuck
  }
  else
  {
    theRegister = 0; // fill the register with a dummy value
  }

  Wire.requestFrom(address, numBytes); // all bytes are returned in requestFrom

  // check to be sure correct number of bytes were returned by slave
  if (numBytes == Wire.available())
  {
    i2cRxData[0] = address;
    i2cRxData[1] = theRegister;
    for (int i = 0; i < numBytes; i++)
    {
      i2cRxData[2 + i] = Wire.read();
    }
  }
  else
  {
    if (numBytes > Wire.available())
    {
      Firmata.sendString("I2C Read Error: Too many bytes received");
    }
    else
    {
      Firmata.sendString("I2C Read Error: Too few bytes received");
    }
  }

  // send slave address, register and received bytes
  Firmata.sendSysex(SYSEX_I2C_REPLY, numBytes + 2, i2cRxData);
}

void outputPort(byte portNumber, byte portValue, byte forceSend)
{
  // pins not configured as INPUT are cleared to zeros
  portValue = portValue & portConfigInputs[portNumber];
  // only send if the value is different than previously sent
  if (forceSend || previousPINs[portNumber] != portValue)
  {
    Firmata.sendDigitalPort(portNumber, portValue);
    previousPINs[portNumber] = portValue;
  }
}

/* -----------------------------------------------------------------------------
 * check all the active digital inputs for change of state, then add any events
 * to the Serial output queue using Serial.print() */
void checkDigitalInputs(void)
{
  /* Using non-looping code allows constants to be given to readPort().
   * The compiler will apply substantial optimizations if the inputs
   * to readPort() are compile-time constants. */
  if (TOTAL_PORTS > 0 && reportPINs[0])
    outputPort(0, readPort(0, portConfigInputs[0]), false);
  if (TOTAL_PORTS > 1 && reportPINs[1])
    outputPort(1, readPort(1, portConfigInputs[1]), false);
  if (TOTAL_PORTS > 2 && reportPINs[2])
    outputPort(2, readPort(2, portConfigInputs[2]), false);
  if (TOTAL_PORTS > 3 && reportPINs[3])
    outputPort(3, readPort(3, portConfigInputs[3]), false);
  if (TOTAL_PORTS > 4 && reportPINs[4])
    outputPort(4, readPort(4, portConfigInputs[4]), false);
  if (TOTAL_PORTS > 5 && reportPINs[5])
    outputPort(5, readPort(5, portConfigInputs[5]), false);
  if (TOTAL_PORTS > 6 && reportPINs[6])
    outputPort(6, readPort(6, portConfigInputs[6]), false);
  if (TOTAL_PORTS > 7 && reportPINs[7])
    outputPort(7, readPort(7, portConfigInputs[7]), false);
  if (TOTAL_PORTS > 8 && reportPINs[8])
    outputPort(8, readPort(8, portConfigInputs[8]), false);
  if (TOTAL_PORTS > 9 && reportPINs[9])
    outputPort(9, readPort(9, portConfigInputs[9]), false);
  if (TOTAL_PORTS > 10 && reportPINs[10])
    outputPort(10, readPort(10, portConfigInputs[10]), false);
  if (TOTAL_PORTS > 11 && reportPINs[11])
    outputPort(11, readPort(11, portConfigInputs[11]), false);
  if (TOTAL_PORTS > 12 && reportPINs[12])
    outputPort(12, readPort(12, portConfigInputs[12]), false);
  if (TOTAL_PORTS > 13 && reportPINs[13])
    outputPort(13, readPort(13, portConfigInputs[13]), false);
  if (TOTAL_PORTS > 14 && reportPINs[14])
    outputPort(14, readPort(14, portConfigInputs[14]), false);
  if (TOTAL_PORTS > 15 && reportPINs[15])
    outputPort(15, readPort(15, portConfigInputs[15]), false);
}

// -----------------------------------------------------------------------------
/* sets the pin mode to the correct state and sets the relevant bits in the
 * two bit-arrays that track Digital I/O and PWM status
 */
void setPinModeCallback(byte pin, int mode)
{
  if (pinConfig[pin] == I2C && isI2CEnabled && mode != I2C)
  {
    // disable i2c so pins can be used for other functions
    // the following if statements should reconfigure the pins properly
    disableI2CPins();
  }
  if (IS_PIN_SERVO(pin) && mode != SERVO && servos[PIN_TO_SERVO(pin)].attached())
  {
    servos[PIN_TO_SERVO(pin)].detach();
  }
  if (IS_PIN_ANALOG(pin))
  {
    reportAnalogCallback(PIN_TO_ANALOG(pin), mode == ANALOG ? 1 : 0); // turn on/off reporting
  }
  if (IS_PIN_DIGITAL(pin))
  {
    if (mode == INPUT)
    {
      portConfigInputs[pin / 8] |= (1 << (pin & 7));
    }
    else
    {
      portConfigInputs[pin / 8] &= ~(1 << (pin & 7));
    }
  }
  pinState[pin] = 0;
  switch (mode)
  {
  case ANALOG:
    if (IS_PIN_ANALOG(pin))
    {
      if (IS_PIN_DIGITAL(pin))
      {
        pinMode(PIN_TO_DIGITAL(pin), INPUT);    // disable output driver
        digitalWrite(PIN_TO_DIGITAL(pin), LOW); // disable internal pull-ups
      }
      pinConfig[pin] = ANALOG;
    }
    break;
  case INPUT:
    if (IS_PIN_DIGITAL(pin))
    {
      pinMode(PIN_TO_DIGITAL(pin), INPUT);    // disable output driver
      digitalWrite(PIN_TO_DIGITAL(pin), LOW); // disable internal pull-ups
      pinConfig[pin] = INPUT;
    }
    break;
  case OUTPUT:
    if (IS_PIN_DIGITAL(pin))
    {
      digitalWrite(PIN_TO_DIGITAL(pin), LOW); // disable PWM
      pinMode(PIN_TO_DIGITAL(pin), OUTPUT);
      pinConfig[pin] = OUTPUT;
    }
    break;
  case PWM:
    if (IS_PIN_PWM(pin))
    {
      pinMode(PIN_TO_PWM(pin), OUTPUT);
      analogWrite(PIN_TO_PWM(pin), 0);
      pinConfig[pin] = PWM;
    }
    break;
  case SERVO:
    if (IS_PIN_SERVO(pin))
    {
      pinConfig[pin] = SERVO;
      if (!servos[PIN_TO_SERVO(pin)].attached())
      {
        servos[PIN_TO_SERVO(pin)].attach(PIN_TO_DIGITAL(pin));
      }
    }
    break;
  case I2C:
    if (IS_PIN_I2C(pin))
    {
      // mark the pin as i2c
      // the user must call I2C_CONFIG to enable I2C for a device
      pinConfig[pin] = I2C;
    }
    break;
  default:
    Firmata.sendString("Unknown pin mode"); // TODO: put error msgs in EEPROM
  }
  // TODO: save status to EEPROM here, if changed
}

void analogWriteCallback(byte pin, int value)
{
  if (pin < TOTAL_PINS)
  {
    switch (pinConfig[pin])
    {
    case SERVO:
      if (IS_PIN_SERVO(pin))
        servos[PIN_TO_SERVO(pin)].write(value);
      pinState[pin] = value;
      break;
    case PWM:
      if (IS_PIN_PWM(pin))
        analogWrite(PIN_TO_PWM(pin), value);
      pinState[pin] = value;
      break;
    }
  }
}

void digitalWriteCallback(byte port, int value)
{
  byte pin, lastPin, mask = 1, pinWriteMask = 0;

  if (port < TOTAL_PORTS)
  {
    // create a mask of the pins on this port that are writable.
    lastPin = port * 8 + 8;
    if (lastPin > TOTAL_PINS)
      lastPin = TOTAL_PINS;
    for (pin = port * 8; pin < lastPin; pin++)
    {
      // do not disturb non-digital pins (eg, Rx & Tx)
      if (IS_PIN_DIGITAL(pin))
      {
        // only write to OUTPUT and INPUT (enables pullup)
        // do not touch pins in PWM, ANALOG, SERVO or other modes
        if (pinConfig[pin] == OUTPUT || pinConfig[pin] == INPUT)
        {
          pinWriteMask |= mask;
          pinState[pin] = ((byte)value & mask) ? 1 : 0;
        }
      }
      mask = mask << 1;
    }
    writePort(port, (byte)value, pinWriteMask);
  }
}

// -----------------------------------------------------------------------------
/* sets bits in a bit array (int) to toggle the reporting of the analogIns
 */
//void FirmataClass::setAnalogPinReporting(byte pin, byte state) {
//}
void reportAnalogCallback(byte analogPin, int value)
{
  if (analogPin < TOTAL_ANALOG_PINS)
  {
    if (value == 0)
    {
      analogInputsToReport = analogInputsToReport & ~(1 << analogPin);
    }
    else
    {
      analogInputsToReport = analogInputsToReport | (1 << analogPin);
    }
  }
  // TODO: save status to EEPROM here, if changed
}

void reportDigitalCallback(byte port, int value)
{
  if (port < TOTAL_PORTS)
  {
    reportPINs[port] = (byte)value;
  }
  // do not disable analog reporting on these 8 pins, to allow some
  // pins used for digital, others analog.  Instead, allow both types
  // of reporting to be enabled, but check if the pin is configured
  // as analog when sampling the analog inputs.  Likewise, while
  // scanning digital pins, portConfigInputs will mask off values from any
  // pins configured as analog
}

/*==============================================================================
 * SYSEX-BASED commands
 *============================================================================*/

void sysexCallback(byte command, byte argc, byte *argv)
{
  byte mode;
  byte slaveAddress;
  byte slaveRegister;
  byte data;
  unsigned int delayTime;

  switch (command)
  {
  case I2C_REQUEST:
    mode = argv[1] & I2C_READ_WRITE_MODE_MASK;
    if (argv[1] & I2C_10BIT_ADDRESS_MODE_MASK)
    {
      Firmata.sendString("10-bit addressing not supported");
      return;
    }
    else
    {
      slaveAddress = argv[0];
    }

    switch (mode)
    {
    case I2C_WRITE:
      Wire.beginTransmission(slaveAddress);
      for (byte i = 2; i < argc; i += 2)
      {
        data = argv[i] + (argv[i + 1] << 7);
        Wire.write((byte)data);
      }
      Wire.endTransmission();
      delayMicroseconds(70);

      break;

    case I2C_READ:
    case I2C_READ_CONTINUOUSLY:
      if (argc == 6)
      {
        // a slave register is specified
        slaveRegister = argv[2] + (argv[3] << 7);
        data = argv[4] + (argv[5] << 7); // bytes to read
      }
      else
      {
        // a slave register is NOT specified
        slaveRegister = (int)REGISTER_NOT_SPECIFIED;
        data = argv[2] + (argv[3] << 7); // bytes to read
      }

      if (mode == I2C_READ)
      {
        readAndReportData(slaveAddress, slaveRegister, data);
      }
      else
      {
        if ((queryIndex + 1) >= MAX_QUERIES)
        {
          Firmata.sendString("too many queries");
          break;
        }
        queryIndex++;
        query[queryIndex].addr = slaveAddress;
        query[queryIndex].reg = slaveRegister;
        query[queryIndex].bytes = data;
      }

      break;

    case I2C_STOP_READING:
      byte queryIndexToSkip;
      // if read continuous mode is enabled for only 1 i2c device, disable
      // read continuous reporting for that device
      if (queryIndex <= 0)
      {
        queryIndex = -1;
      }
      else
      {
        // if read continuous mode is enabled for multiple devices,
        // determine which device to stop reading and remove it's data from
        // the array, shifiting other array data to fill the space
        for (byte i = 0; i < queryIndex + 1; i++)
        {
          if (query[i].addr == slaveAddress)
          {
            queryIndexToSkip = i;

            break;
          }
        }

        for (byte i = queryIndexToSkip; i < queryIndex + 1; i++)
        {
          if (i < MAX_QUERIES)
          {
            query[i].addr = query[i + 1].addr;
            query[i].reg = query[i + 1].reg;
            query[i].bytes = query[i + 1].bytes;
          }
        }
        queryIndex--;
      }

      break;
    default:

      break;
    }
    break;
  case I2C_CONFIG:
    delayTime = (argv[0] + (argv[1] << 7));

    if (delayTime > 0)
    {
      i2cReadDelayTime = delayTime;
    }

    if (!isI2CEnabled)
    {
      enableI2CPins();
    }

    break;
  case SERVO_CONFIG:
    if (argc > 4)
    {
      // these vars are here for clarity, they'll optimized away by the compiler
      byte pin = argv[0];
      int minPulse = argv[1] + (argv[2] << 7);
      int maxPulse = argv[3] + (argv[4] << 7);

      if (IS_PIN_SERVO(pin))
      {
        if (servos[PIN_TO_SERVO(pin)].attached())
        {
          servos[PIN_TO_SERVO(pin)].detach();
        }
        servos[PIN_TO_SERVO(pin)].attach(PIN_TO_DIGITAL(pin), minPulse, maxPulse);
        setPinModeCallback(pin, SERVO);
      }
    }
    break;
  case SAMPLING_INTERVAL:
    if (argc > 1)
    {
      samplingInterval = argv[0] + (argv[1] << 7);
      if (samplingInterval < MINIMUM_SAMPLING_INTERVAL)
      {
        samplingInterval = MINIMUM_SAMPLING_INTERVAL;
      }
    }
    else
    {
      //Firmata.sendString("Not enough data");
    }
    break;
  case EXTENDED_ANALOG:
    if (argc > 1)
    {
      int val = argv[1];
      if (argc > 2)
      {
        val |= (argv[2] << 7);
      }
      if (argc > 3)
      {
        val |= (argv[3] << 14);
      }
      analogWriteCallback(argv[0], val);
    }
    break;
  case CAPABILITY_QUERY:
    Serial.write(START_SYSEX);
    Serial.write(CAPABILITY_RESPONSE);
    for (byte pin = 0; pin < TOTAL_PINS; pin++)
    {
      if (IS_PIN_DIGITAL(pin))
      {
        Serial.write((byte)INPUT);
        Serial.write(1);
        Serial.write((byte)OUTPUT);
        Serial.write(1);
        Serial.write((byte)PING_READ);
        Serial.write(1);
      }
      if (IS_PIN_ANALOG(pin))
      {
        Serial.write(ANALOG);
        Serial.write(10);
      }
      if (IS_PIN_PWM(pin))
      {
        Serial.write(PWM);
        Serial.write(8);
      }
      if (IS_PIN_SERVO(pin))
      {
        Serial.write(SERVO);
        Serial.write(14);
      }
      if (IS_PIN_I2C(pin))
      {
        Serial.write(I2C);
        Serial.write(1); // to do: determine appropriate value
      }
      Serial.write(127);
    }
    Serial.write(END_SYSEX);
    break;
  case PIN_STATE_QUERY:
    if (argc > 0)
    {
      byte pin = argv[0];
      Serial.write(START_SYSEX);
      Serial.write(PIN_STATE_RESPONSE);
      Serial.write(pin);
      if (pin < TOTAL_PINS)
      {
        Serial.write((byte)pinConfig[pin]);
        Serial.write((byte)pinState[pin] & 0x7F);
        if (pinState[pin] & 0xFF80)
        {
          Serial.write((byte)(pinState[pin] >> 7) & 0x7F);
        }
        if (pinState[pin] & 0xC000)
        {
          Serial.write((byte)(pinState[pin] >> 14) & 0x7F);
        }
      }
      Serial.write(END_SYSEX);
    }
    break;
  case ANALOG_MAPPING_QUERY:
    Serial.write(START_SYSEX);
    Serial.write(ANALOG_MAPPING_RESPONSE);
    for (byte pin = 0; pin < TOTAL_PINS; pin++)
    {
      Serial.write(IS_PIN_ANALOG(pin) ? PIN_TO_ANALOG(pin) : 127);
    }
    Serial.write(END_SYSEX);
    break;
  case PING_READ:
  {
    byte pulseDurationArray[4] = {
        (argv[2] & 0x7F) | ((argv[3] & 0x7F) << 7),
        (argv[4] & 0x7F) | ((argv[5] & 0x7F) << 7),
        (argv[6] & 0x7F) | ((argv[7] & 0x7F) << 7),
        (argv[8] & 0x7F) | ((argv[9] & 0x7F) << 7)};
    unsigned long pulseDuration = ((unsigned long)pulseDurationArray[0] << 24) + ((unsigned long)pulseDurationArray[1] << 16) + ((unsigned long)pulseDurationArray[2] << 8) + ((unsigned long)pulseDurationArray[3]);
    if (argv[1] == HIGH)
    {
      pinMode(argv[0], OUTPUT);
      digitalWrite(argv[0], LOW);
      delayMicroseconds(2);
      digitalWrite(argv[0], HIGH);
      delayMicroseconds(pulseDuration);
      digitalWrite(argv[0], LOW);
    }
    else
    {
      digitalWrite(argv[0], HIGH);
      delayMicroseconds(2);
      digitalWrite(argv[0], LOW);
      delayMicroseconds(pulseDuration);
      digitalWrite(argv[0], HIGH);
    }
    unsigned long duration;
    byte responseArray[5];
    byte timeoutArray[4] = {
        (argv[10] & 0x7F) | ((argv[11] & 0x7F) << 7),
        (argv[12] & 0x7F) | ((argv[13] & 0x7F) << 7),
        (argv[14] & 0x7F) | ((argv[15] & 0x7F) << 7),
        (argv[16] & 0x7F) | ((argv[17] & 0x7F) << 7)};
    unsigned long timeout = ((unsigned long)timeoutArray[0] << 24) +
                            ((unsigned long)timeoutArray[1] << 16) +
                            ((unsigned long)timeoutArray[2] << 8) +
                            ((unsigned long)timeoutArray[3]);

    pinMode(argv[0], INPUT);
    duration = pulseIn(argv[0], argv[1], timeout);
    responseArray[0] = argv[0];
    responseArray[1] = (((unsigned long)duration >> 24) & 0xFF);
    responseArray[2] = (((unsigned long)duration >> 16) & 0xFF);
    responseArray[3] = (((unsigned long)duration >> 8) & 0xFF);
    responseArray[4] = (((unsigned long)duration & 0xFF));

    Firmata.sendSysex(PING_READ, 5, responseArray);
    break;
  }
  }
}

void enableI2CPins()
{
  for (byte i = 0; i < TOTAL_PINS; i++)
  {
    if (IS_PIN_I2C(i))
    {
      // mark pins as i2c so they are ignore in non i2c data requests
      setPinModeCallback(i, I2C);
    }
  }

  isI2CEnabled = true;

  // is there enough time before the first I2C request to call this here?
  Wire.begin();
}

void disableI2CPins()
{
  isI2CEnabled = false;
  queryIndex = -1;
}

void systemResetCallback()
{
  if (isI2CEnabled)
  {
    disableI2CPins();
  }
  for (byte i = 0; i < TOTAL_PORTS; i++)
  {
    reportPINs[i] = false;   // by default, reporting off
    portConfigInputs[i] = 0; // until activated
    previousPINs[i] = 0;
  }
  // pins with analog capability default to analog input
  // otherwise, pins default to digital output
  for (byte i = 0; i < TOTAL_PINS; i++)
  {
    if (IS_PIN_ANALOG(i))
    {
      // turns off pullup, configures everything
      setPinModeCallback(i, ANALOG);
    }
    else
    {
      // sets the output to 0, configures portConfigInputs
      setPinModeCallback(i, OUTPUT);
    }
  }
  // by default, do not report any analog inputs
  analogInputsToReport = 0;
}

void setup()
{
  Firmata.setFirmwareVersion(FIRMATA_MAJOR_VERSION, FIRMATA_MINOR_VERSION);
  Firmata.attach(ANALOG_MESSAGE, analogWriteCallback);
  Firmata.attach(DIGITAL_MESSAGE, digitalWriteCallback);
  Firmata.attach(REPORT_ANALOG, reportAnalogCallback);
  Firmata.attach(REPORT_DIGITAL, reportDigitalCallback);
  Firmata.attach(SET_PIN_MODE, setPinModeCallback);
  Firmata.attach(START_SYSEX, sysexCallback);
  Firmata.attach(SYSTEM_RESET, systemResetCallback);

  Firmata.begin(57600);
  systemResetCallback(); // reset to default config
}

void loop()
{
  byte pin, analogPin;

  /* DIGITALREAD - as fast as possible, check for changes and output them to the
   * FTDI buffer using Serial.print()  */
  checkDigitalInputs();

  /* SERIALREAD - processing incoming messagse as soon as possible, while still
   * checking digital inputs.  */
  while (Firmata.available())
  {
    Firmata.processInput();
  }

  /* SEND FTDI WRITE BUFFER - make sure that the FTDI buffer doesn't go over
   * 60 bytes. use a timer to sending an event character every 4 ms to
   * trigger the buffer to dump. */

  currentMillis = millis();
  if (currentMillis - previousMillis > samplingInterval)
  {
    previousMillis += samplingInterval;
    /* ANALOGREAD - do all analogReads() at the configured sampling interval */
    for (pin = 0; pin < TOTAL_PINS; pin++)
    {
      if (IS_PIN_ANALOG(pin) && pinConfig[pin] == ANALOG)
      {
        analogPin = PIN_TO_ANALOG(pin);
        if (analogInputsToReport & (1 << analogPin))
        {
          Firmata.sendAnalog(analogPin, analogRead(analogPin));
        }
      }
    }
    // report i2c data for all device with read continuous mode enabled
    if (queryIndex > -1)
    {
      for (byte i = 0; i < queryIndex + 1; i++)
      {
        readAndReportData(query[i].addr, query[i].reg, query[i].bytes);
      }
    }
  }
}
MKR1000 SketchC#
This sketch sends the majority of sensor readings to the app running on the Raspberry Pi. Make sure to update the Wifi SSID and Password before uploading.
#include <OneWire.h>
#include <DallasTemperature.h>
#include <ArduinoHttpClient.h>
#include <WiFi101.h>
#include <SimpleDHT.h>
// Using Pin 5 of MKR1000
#define ONE_WIRE_BUS_PIN 5
// TODO: Move out ot separate file
#define SECRET_SSID "SECRET_SSID"
#define SECRET_PASS "SECRET_PASS"
#define RIG_NAME "Gill"

char ssid[] = SECRET_SSID;
char pass[] = SECRET_PASS;

// Setup a oneWire instance for temperature probes
OneWire oneWire(ONE_WIRE_BUS_PIN);
// Pass our oneWire reference to Dallas Temperature.
DallasTemperature sensors(&oneWire);

// probe_a: "28 FF 2F 9C B0 16 3 34"
// probe_b: "28 FF 36 1E B1 16 4 4D"
// probe_c: "28 FF 27 1E B1 16 4 FC"
// probe_d: "28 FF 6A 74 B0 16 5 87"
// probe_e: "28 FF  E B5 B0 16 3 E2"

// Define device addresses for each probe
DeviceAddress Probe01 = {0x28, 0xFF, 0x2F, 0x9C, 0xB0, 0x16, 0x03, 0x34};
DeviceAddress Probe02 = {0x28, 0xFF, 0x36, 0x1E, 0xB1, 0x16, 0x04, 0x4D};
DeviceAddress Probe03 = {0x28, 0xFF, 0x27, 0x1E, 0xB1, 0x16, 0x04, 0xFC};
DeviceAddress Probe04 = {0x28, 0xFF, 0x6A, 0x74, 0xB0, 0x16, 0x05, 0x87};
DeviceAddress Probe05 = {0x28, 0xFF, 0x0E, 0xB5, 0xB0, 0x16, 0x03, 0xE2};

int uvSensor = A1;
int uvIndex = 0;

int pinDHT22 = A2;
SimpleDHT22 dht22(pinDHT22);

int soilSensor = A3;
int soilMoisture = 0;

char serverAddress[] = "192.168.86.127"; // raspberry pi address
int port = 3030;

WiFiClient wifi;
HttpClient client = HttpClient(wifi, serverAddress, port);
int status = WL_IDLE_STATUS;
String response;
int statusCode = 0;

void setup()
{
  // start serial port to show results
  Serial.begin(9600);
  delay(3000);

  pinMode(LED_BUILTIN, OUTPUT);

  while (status != WL_CONNECTED)
  {
    Serial.print("Attempting to connect to Network named: ");
    Serial.println(ssid); // print the network name (SSID);

    // Connect to WPA/WPA2 network:
    status = WiFi.begin(ssid, pass);
  }

  // print the SSID of the network you're attached to:
  Serial.print("SSID: ");
  Serial.println(WiFi.SSID());

  // print your WiFi shield's IP address:
  IPAddress ip = WiFi.localIP();
  Serial.print("IP Address: ");
  Serial.println(ip);

  Serial.print("Initializing Temperature Control Library Version ");
  Serial.println(DALLASTEMPLIBVERSION);

  // Initialize the Temperature measurement library
  sensors.begin();

  // set the resolution to 10 bit (Can be 9 to 12 bits .. lower is faster)
  sensors.setResolution(Probe01, 9);
  sensors.setResolution(Probe02, 9);
  sensors.setResolution(Probe03, 9);
  sensors.setResolution(Probe04, 9);
  sensors.setResolution(Probe05, 9);
}

void loop() /****** LOOP: RUNS CONSTANTLY ******/
{
  Serial.println();
  Serial.print("Total Probes: ");
  Serial.println(sensors.getDeviceCount());

  // Command all devices on bus to read temperature
  sensors.requestTemperatures();

  float probeA = sensors.getTempC(Probe01);
  float probeB = sensors.getTempC(Probe02);
  float probeC = sensors.getTempC(Probe03);
  float probeD = sensors.getTempC(Probe04);
  float probeE = sensors.getTempC(Probe05);

  float moistureSensorValue = analogRead(soilSensor);
  soilMoisture = ((moistureSensorValue / 1024) - 1) * 100 * -1;

  float uvSensorValue = analogRead(uvSensor);
  uvIndex = uvSensorValue / 1024 * 3.3 / 0.1;

  Serial.print("Rig Name: ");
  Serial.println(String(RIG_NAME));

  Serial.print("ProbeA:  ");
  printTemperature(Probe01);
  Serial.println();

  Serial.print("ProbeB:  ");
  printTemperature(Probe02);
  Serial.println();

  Serial.print("ProbeC:  ");
  printTemperature(Probe03);
  Serial.println();

  Serial.print("ProbeD:  ");
  printTemperature(Probe04);
  Serial.println();

  Serial.print("ProbeE:  ");
  printTemperature(Probe05);
  Serial.println();

  Serial.print("soilMoisture:  ");
  Serial.print(soilMoisture);
  Serial.println();

  Serial.print("uvIndex:  ");
  Serial.print(uvIndex);
  Serial.println();

  byte temperature = 0;
  byte humidity = 0;
  int err = SimpleDHTErrSuccess;
  if ((err = dht22.read(&temperature, &humidity, NULL)) != SimpleDHTErrSuccess)
  {
    Serial.print("Read DHT22 failed, err=");
    Serial.println(err);
  }
  else
  {
    Serial.print("DHT22: ");
    Serial.print((int)temperature);
    Serial.print(" *C, ");
    Serial.print((int)humidity);
    Serial.println(" RH%");
  }

  String postURL = String("POST readings to " + String(serverAddress) + ':' + String(port));
  Serial.println(postURL);
  String contentType = "application/x-www-form-urlencoded";
  String postData = String(
      "probeA=" + String(probeA) +
      "&probeB=" + String(probeB) +
      "&probeC=" + String(probeC) +
      "&probeD=" + String(probeD) +
      "&probeE=" + String(probeE) +
      "&rig_name=" + String(RIG_NAME) +
      "&uvIndex=" + String(uvIndex) +
      "&soilMoisture=" + String(soilMoisture) +
      "&humidity=" + String(humidity) +
      "&temperature=" + String(temperature));

  digitalWrite(LED_BUILTIN, HIGH);
  client.post("/temperatures", contentType, postData);

  // read the status code and body of the response
  statusCode = client.responseStatusCode();
  response = client.responseBody();

  Serial.print("Status code: ");
  Serial.println(statusCode);
  Serial.print("Response: ");
  Serial.println(response);

  digitalWrite(LED_BUILTIN, LOW);
  delay(100);
  digitalWrite(LED_BUILTIN, HIGH);
  delay(100);
  digitalWrite(LED_BUILTIN, LOW);
  delay(100);
  digitalWrite(LED_BUILTIN, HIGH);
  delay(100);
  digitalWrite(LED_BUILTIN, LOW);
  delay(1000);
}

// print temperature for device adress
void printTemperature(DeviceAddress deviceAddress)
{

  float tempC = sensors.getTempC(deviceAddress);

  if (tempC == -127.00)
  {
    Serial.print("Error getting temperature  ");
  }
  else
  {
    Serial.print(tempC, 1);
    Serial.print(" C");
    // Serial.print(" F: ");
    // Serial.print(DallasTemperature::toFahrenheit(tempC));
  }
}
SmartTerrariumClient
This is the Client portion of Smart Terrarium built using Vue.js.
SmartTerrariumApp
Smart Terrarium App for receiving data, controlling hardware, and running the Smart Terrarium Client. This repo also contains the sketch for the MKR1000.

Custom parts and enclosures

Mister Mount
This provides a nice clean installation of the misting head for the misting system.
Plant Holder Brace
This provides extra support to the cage for hanging a plant.

Schematics

Arduino Uno diagram
The Arduino Uno circuit used to control both relays for the lighting and misting system, as well as monitor the water levels on the misting bucket and drain.
Uno with proximity sensors i5wvzy3dcf fzdq6vzfsi
Arduino MKR1000
This is really for the MKR1000 but tinkercad circuits didn't have it as a component. All pins are still correct.
Mkr1000 diagram cx8w0wf2w2 jhfnubdnqn

Comments

Similar projects you might like

Smart Terrarium

Project showcase by Ryan Gill

  • 206 views
  • 0 comments
  • 10 respects

Smart Battery Charger Multiplexer with Smart Display

Project tutorial by leonzak

  • 2,121 views
  • 4 comments
  • 12 respects

Smart Garden

Project showcase by patel Dipen

  • 30,327 views
  • 16 comments
  • 73 respects

Smart Home - Smart Rules using ARTIK Cloud & Photon

Project tutorial by Raghavendra Ponuganti

  • 4,316 views
  • 2 comments
  • 14 respects

Smart Health Kit

Project showcase by Team Akatsuki

  • 3,716 views
  • 1 comment
  • 14 respects
Add projectSign up / Login