Brian RashapRussell BrazellJessica Rodriquez
Published © MIT

Smart Hydroponics

An autonomic hydroponics unit with everything you need for a full-size unit packed into a compact corner unit.

ExpertShowcase (no instructions)Over 10 days266
Smart Hydroponics

Things used in this project

Hardware components

PHPoC Bread Board
PHPoC Bread Board
×1
Argon
Particle Argon
×1
SparkFun Spectral Sensor Breakout - AS7262 Visible (Qwiic)
SparkFun Spectral Sensor Breakout - AS7262 Visible (Qwiic)
×1
Flow Sensor, Analog Output
Flow Sensor, Analog Output
×1
EZO-PMP™ Peristaltic Pump
Atlas Scientific EZO-PMP™ Peristaltic Pump
×1
Consumer Grade pH Probe
Atlas Scientific Consumer Grade pH Probe
×1
Conductivity K1.0 Probe
Atlas Scientific Conductivity K1.0 Probe
×1
Solenoid Ball Valve
×1
Grove - 8-Channel Solid State Relay
Seeed Studio Grove - 8-Channel Solid State Relay
×1

Software apps and online services

Particle Build Web IDE
Particle Build Web IDE
VS Code
Microsoft VS Code

Hand tools and fabrication machines

powder coating
Mig Welder

Story

Read more

Custom parts and enclosures

Pipe Brackets

Schematics

Hydroponics System Schematics

Code

Main code

C/C++
Current code that runs the unit
#include "credentials.h"
#include "IOTTimer.h"
#include "neopixel.h"
#include "par_sensor.h"
#include <Adafruit_MQTT.h>
#include "Adafruit_MQTT/Adafruit_MQTT.h"
#include "Adafruit_MQTT/Adafruit_MQTT_SPARK.h"

SYSTEM_MODE(AUTOMATIC);
SYSTEM_THREAD(ENABLED);

// A0 For future O2 sensor
#define PH_PIN A2 // pin for pH meter
#define EC_PIN A1 // pin for EC
#define WaterLevel A3
#define FlowMeter A4
#define PIXEL_PIN A5
#define WATER_LEVEL_HOT_PIN D10
#define Relay_8_Pin D9 // Not currently used
#define PhUpPumpPin D8
#define PhDownPumpPin D7
#define nutrientPump1 D6
#define nutrientPump2 D5
#define TopDrain D4
#define BottomDrain D3
#define FreshWaterPump D2
// D0 and D1 are for I2C
#define PAR_ADDR 0x39

#define PIXEL_COUNT 63
#define PIXEL_TYPE WS2812B

#define SERIESRESISTOR 560

#define Offset 40.349605 // deviation compensate for pH meter
#define samplingInterval 20
#define printInterval 800
#define ArrayLenth 40 // times of collection

const float K_pHCalc = -2.994219;
const float HIGH_PH_LIMIT = 5.5f;
const float LOW_PH_LIMIT = 4.0f;
const float LOW_WATER_LIMIT = 1925.0f; // This is about 1.5 inches of water
const float EMPTY_MIX_TANK = 1880.0f;
const float HIGH_WATER_LIMIT = 2600.0f;
const float HIGH_WATER_INCHES = 8.5f;
const float ML_PER_COUNT = 2.25;

static float waterLevelArr[60];

static float pHValue, voltage, EC;
static float mixTankLevel;

float waterFlowVolume;
float waterLevelAverage = 0;
float waterLevelInches;

const double MLR_B_0 = -4.8853; // This should be -9.8853 to be accurate in sunlight, however inside that is too much correction
const double MLR_B_1 = 0.0046;
const double MLR_B_2 = 0.0136;
const double MLR_B_3 = 0.0243;
const double MLR_B_4 = 0.0459;
const double MLR_B_5 = -0.0471;
const double MLR_B_6 = 0.0195;
const double MLR_B_7 = 0.0178;
const double MLR_B_8 = -0.0026;

static double mlrProduct;

static unsigned long samplingTime, printTime, last, lastTime;

const int EC_PUMP_RUNTIME = 3000;
const int PH_PUMP_RUNTIME = 3000;
const int DRAIN_RUNTIME = 5000;
const int FRESH_PUMP_RUNTIME = 15000;

int pHArrayIndex = 0;
int blue;
int green;
int red;
int channelIndex = 0;
int waterFlowCount;
int waterArrIndex = 0;

int all10Channels[10];
int pHArray[ArrayLenth]; // Store the average value of the pH sensor feedback

IOTTimer connectTimer;
IOTTimer flowTimer;
IOTTimer waterLevelTimer;
IOTTimer nutrientPumpTimer;
IOTTimer delayTimer;

Adafruit_AS7341 as7341;

Adafruit_NeoPixel strip(PIXEL_COUNT, PIXEL_PIN, PIXEL_TYPE);

// MQTT Constructors
TCPClient TheClient;

Adafruit_MQTT_SPARK mqtt(&TheClient, AIO_SERVER, AIO_SERVERPORT, AIO_USERNAME, AIO_KEY);

// Publish
Adafruit_MQTT_Publish mqttPubHydropnicsPARStatus = Adafruit_MQTT_Publish(&mqtt, AIO_USERNAME "/feeds/hydro.parstatus");
Adafruit_MQTT_Publish mqttPubHydropnicsWaterLevelStatus = Adafruit_MQTT_Publish(&mqtt, AIO_USERNAME "/feeds/hydro.waterlevelstatus");
Adafruit_MQTT_Publish mqttPubHydropnicsWaterLevelPercentStatus = Adafruit_MQTT_Publish(&mqtt, AIO_USERNAME "/feeds/hydro.waterlevelpercent");
Adafruit_MQTT_Publish mqttPubHydropnicsFlowStatus = Adafruit_MQTT_Publish(&mqtt, AIO_USERNAME "/feeds/hydro.flowstatus");
Adafruit_MQTT_Publish mqttPubHydropnicsPHStatus = Adafruit_MQTT_Publish(&mqtt, AIO_USERNAME "/feeds/hydro.phstatus");
Adafruit_MQTT_Publish mqttPubHydropnicsECStatus = Adafruit_MQTT_Publish(&mqtt, AIO_USERNAME "/feeds/hydro.ecstatus");

Adafruit_MQTT_Publish mqttPubCh0 = Adafruit_MQTT_Publish(&mqtt, AIO_USERNAME "/feeds/hydro.ch0");
Adafruit_MQTT_Publish mqttPubCh1 = Adafruit_MQTT_Publish(&mqtt, AIO_USERNAME "/feeds/hydro.ch1");
Adafruit_MQTT_Publish mqttPubCh2 = Adafruit_MQTT_Publish(&mqtt, AIO_USERNAME "/feeds/hydro.ch2");
Adafruit_MQTT_Publish mqttPubCh3 = Adafruit_MQTT_Publish(&mqtt, AIO_USERNAME "/feeds/hydro.ch3");
Adafruit_MQTT_Publish mqttPubCh4 = Adafruit_MQTT_Publish(&mqtt, AIO_USERNAME "/feeds/hydro.ch4");
Adafruit_MQTT_Publish mqttPubCh5 = Adafruit_MQTT_Publish(&mqtt, AIO_USERNAME "/feeds/hydro.ch5");
Adafruit_MQTT_Publish mqttPubCh6 = Adafruit_MQTT_Publish(&mqtt, AIO_USERNAME "/feeds/hydro.ch6");
Adafruit_MQTT_Publish mqttPubCh7 = Adafruit_MQTT_Publish(&mqtt, AIO_USERNAME "/feeds/hydro.ch7");
Adafruit_MQTT_Publish mqttPubCh8 = Adafruit_MQTT_Publish(&mqtt, AIO_USERNAME "/feeds/hydro.ch8");
Adafruit_MQTT_Publish mqttPubCh9 = Adafruit_MQTT_Publish(&mqtt, AIO_USERNAME "/feeds/hydro.ch9");



Adafruit_MQTT_Subscribe mqttSubNutrientPumps = Adafruit_MQTT_Subscribe(&mqtt, AIO_USERNAME "/feeds/nutrient-pumps");
Adafruit_MQTT_Subscribe mqttSubPhUpPump = Adafruit_MQTT_Subscribe(&mqtt, AIO_USERNAME "/feeds/phup");
Adafruit_MQTT_Subscribe mqttSubPhDownPump = Adafruit_MQTT_Subscribe(&mqtt, AIO_USERNAME "/feeds/phdown");
Adafruit_MQTT_Subscribe mqttSubFreshWaterRefillPump = Adafruit_MQTT_Subscribe(&mqtt, AIO_USERNAME "/feeds/freshwaterrefill");

void setup()
{
  Serial.begin(115200);
  waitFor(Serial.isConnected, 1000);
  delay(500);
  Serial.printf("***\n*  Booting up the autonomous hydroponics unit!  *\n***\n"); // Test the Serial monitor

  pinMode(PhUpPumpPin, OUTPUT);
  digitalWrite(PhUpPumpPin, HIGH);

  pinMode(PhDownPumpPin, OUTPUT);
  digitalWrite(PhDownPumpPin, HIGH);

  pinMode(nutrientPump1, OUTPUT);
  digitalWrite(nutrientPump1, HIGH);

  pinMode(nutrientPump2, OUTPUT);
  digitalWrite(nutrientPump2, HIGH);

  pinMode(TopDrain, OUTPUT);
  digitalWrite(TopDrain, HIGH);

  pinMode(BottomDrain, OUTPUT);
  digitalWrite(BottomDrain, HIGH);

  pinMode(FreshWaterPump, OUTPUT);
  digitalWrite(FreshWaterPump, HIGH);

  pinMode(WATER_LEVEL_HOT_PIN, OUTPUT);
  pinMode(WaterLevel, INPUT_PULLDOWN);
  pinMode(FlowMeter, INPUT);

  strip.begin();
  strip.clear();
  strip.setBrightness(45);
  strip.show();
  as7341.begin();
  delay(150);
  as7341.setATIME(100);
  as7341.setASTEP(999);
  as7341.setGain(AS7341_GAIN_1X);
  delay(150);
  as7341.startReading();
  delay(150);
  readPAR();

  // Setup MQTT subscription for onoff feed.
  mqtt.subscribe(&mqttSubNutrientPumps);
  mqtt.subscribe(&mqttSubPhUpPump);
  mqtt.subscribe(&mqttSubPhDownPump);
  mqtt.subscribe(&mqttSubFreshWaterRefillPump);
}

void loop()
{
  listenForSubs();
  // delay(3000);
  turnGreen();
  listenForSubs();
  readWaterFlow();
  listenForSubs();
  readMixTankLevel();
  listenForSubs();
  pHLevelControl();
  // ECLevelControl();
  readPAR();
  listenForSubs();
  readPh();
  publishReadings();
  listenForSubs();
  openTopDrain();
  listenForSubs();
  openBottomDrain();
}

double avergearray(int *arr, int number)
{
  int i;
  int max, min;
  double avg;
  long amount = 0;
  if (number <= 0)
  {
    Serial.println("Error number for the array to avraging!/n");
    return 0;
  }
  if (number < 5)
  { // less than 5, calculated directly statistics
    for (i = 0; i < number; i++)
    {
      amount += arr[i];
    }
    avg = amount / number;
    return avg;
  }
  else
  {
    if (arr[0] < arr[1])
    {
      min = arr[0];
      max = arr[1];
    }
    else
    {
      min = arr[1];
      max = arr[0];
    }
    for (i = 2; i < number; i++)
    {
      if (arr[i] < min)
      {
        amount += min; // arr<min
        min = arr[i];
      }
      else
      {
        if (arr[i] > max)
        {
          amount += max; // arr>max
          max = arr[i];
        }
        else
        {
          amount += arr[i]; // min<=arr<=max
        }
      } // if
    }   // for
    avg = (double)amount / (number - 2);
  } // if
  return avg;
}

void readPh()
{
  
  
  printTime = millis();

  if (millis() - samplingTime > samplingInterval)
  {
    pHArray[pHArrayIndex++] = analogRead(PH_PIN);
    if (pHArrayIndex == ArrayLenth)
    pHArrayIndex = 0;
    voltage = avergearray(pHArray, ArrayLenth) * 3.3 / 4096;
    pHValue = K_pHCalc * voltage + Offset;
    samplingTime = millis();
  }
  Serial.print("pH value: ");
  Serial.println(pHValue, 2);
}

void PhUpPumpPinOn()
{
  digitalWrite(PhUpPumpPin, LOW);
  Serial.printf("Rising PH Value\n");
  delay(PH_PUMP_RUNTIME);
  digitalWrite(PhUpPumpPin, HIGH);
  Serial.printf("PH Up Off\n");
}

void PhDownPumpPinOn()
{
  digitalWrite(PhDownPumpPin, LOW);
  Serial.printf("Lowering PH Value\n");
  delay(PH_PUMP_RUNTIME);
  digitalWrite(PhDownPumpPin, HIGH);
  Serial.printf("PH Down off\n");
}

void nutrientPump1On()
{
  digitalWrite(nutrientPump1, LOW);
  Serial.printf("Adding Nutrient 1\n");
  delay(EC_PUMP_RUNTIME);
  digitalWrite(nutrientPump1, HIGH);
  Serial.printf("Done adding Nutrient 1\n");
}

void nutientPump2On()
{
  digitalWrite(nutrientPump2, LOW);
  Serial.printf("Adding Nutrient 2\n");
  delay(EC_PUMP_RUNTIME);
  digitalWrite(nutrientPump2, HIGH);
  Serial.printf("Done adding Nutrient 2\n");
}

void openTopDrain()
{
  digitalWrite(TopDrain, LOW);
  Serial.printf("Draining Top Loop\n");
  delay(DRAIN_RUNTIME);
  digitalWrite(TopDrain, HIGH);
  Serial.printf("Done Draining Top Loop\n");
}

void openBottomDrain()
{
  digitalWrite(BottomDrain, LOW);
  Serial.printf("Draining Bottom Loop\n");
  delay(DRAIN_RUNTIME);
  digitalWrite(BottomDrain, HIGH);
  Serial.printf("Done Draining Bottom Loop\n");
}

void freshWaterPumpOn()
{
  digitalWrite(FreshWaterPump, LOW);
  Serial.printf("Running Fresh Water Fill\n");
  delay(FRESH_PUMP_RUNTIME);
  digitalWrite(FreshWaterPump, HIGH);
  Serial.printf("Fresh Water Fill off\n");
}

void Relay_8_PinOn()
{
  digitalWrite(Relay_8_Pin, LOW);
  Serial.printf("Relay 8 on\n");
  delay(FRESH_PUMP_RUNTIME);
  digitalWrite(Relay_8_Pin, HIGH);
  Serial.printf("Relay 8 off\n");
}

void pHLevelControl()
{
  readPh();
  pHValue=5.0; //temp to disable pumps
  if (pHValue > 5.5)
  {
    PhDownPumpPinOn();
  }
  else if (pHValue < 4.5)
  {
    PhUpPumpPinOn();
  }
}

void runNutientPump()
{
  nutrientPumpTimer.startTimer(600000);
  nutrientPump1On();
  while (!nutrientPumpTimer.isTimerReady())
  {
    delayTimer.startTimer(10000);
    Serial.println("Waiting for the nutrient to mix before running the next pump");
    while (!delayTimer.isTimerReady())
      ;
  }
  nutientPump2On();
}

void ECLevelControl()
{
  readMixTankLevel();
  if (EC < 1.2)
  {
    nutrientPumpTimer.startTimer(600000);
    nutrientPump1On();
    while (!nutrientPumpTimer.isTimerReady())
    {
      delayTimer.startTimer(10000);
      Serial.println("Waiting for the nutrient to mix before running the next pump");
      while (!delayTimer.isTimerReady())
        ;
    }
    nutientPump2On();
  }
  else if (EC > 2.0 && mixTankLevel < LOW_WATER_LIMIT)
  {
    freshWaterPumpOn();
  }
}

float readMixTankLevel()
{
  digitalWrite(WATER_LEVEL_HOT_PIN, HIGH);
  delay(100);
  mixTankLevel = (float)analogRead(WaterLevel);
  Serial.printf("*The ANALOG value is %f\n", mixTankLevel);
  digitalWrite(WATER_LEVEL_HOT_PIN, LOW);
  waterLevelInches = map(mixTankLevel, EMPTY_MIX_TANK, HIGH_WATER_LIMIT, 0.0f, 8.1f);
  Serial.printlnf("The tank is at %f inches", waterLevelInches);
  Serial.printlnf("The average water Level is %f\n", averageWaterLevel(waterLevelInches));
  return waterLevelInches;
}

float averageWaterLevel(float waterReading)
{
  float waterLevelSum = 0;
  waterLevelAverage = 0;
  waterLevelArr[waterArrIndex] = waterReading;
  waterArrIndex++;
  if (waterArrIndex == 60)
  {
    for (int i = 0; i < waterArrIndex - 1; i++)
    {
      waterLevelSum += waterLevelArr[i];
    }
    waterLevelAverage = waterLevelSum / (waterArrIndex + 1);
    waterArrIndex = 0;
    publishWaterLevelReadings();
    if (waterLevelAverage >= 4.0 && waterLevelAverage <= 5.5)
    {
      freshWaterPumpOn();
    }
    return waterLevelAverage;
  }
  else
  {
    return 7.777;
  }
}

float readWaterFlow()
{
  Serial.println("Start to read flow rate for 30 seconds");
  waterFlowCount = 0;
  flowTimer.startTimer(30000);
  while (!flowTimer.isTimerReady())
  {
    if (digitalRead(FlowMeter) == LOW)
    {
      waterFlowCount++;
      while (digitalRead(FlowMeter) == LOW)
        ;
    }
  }
  waterFlowVolume = ((waterFlowCount * ML_PER_COUNT) * 2.0) / 1000.0;
  Serial.printf("Current water flow is %f Liters per minute\n", waterFlowVolume);
  return waterFlowVolume;
}

void turnGreen()
{
  Serial.println("Correcting NP color");
  int glowColor = 0x00FFAA;
  if (mlrProduct < 5.0)
  {
    glowColor = 0x0000FF;
  }
  else if (mlrProduct > 5.0 && mlrProduct <= 40.0)
  {
    glowColor = 0x0AFF0A;
  }
  else if (mlrProduct > 40.0)
  {
    glowColor = 0xFF0000;
  }
  strip.clear();
  for (int i = 0; i < 35; i++)
  {
    strip.setPixelColor(i, glowColor);
    strip.show();
  }
}

double readPAR() {
  uint16_t readings[12];
  static unsigned int lastPAR = 0;

  delay(150);
  if (!as7341.readAllChannels(readings))
  {
    Serial.println("Error reading all channels!");
  }
  mlrProduct = MLR_B_0 + (MLR_B_1 * (double)readings[0]) + (MLR_B_2 * (double)readings[1]) + (MLR_B_3 * (double)readings[2]) + (MLR_B_4 * (double)readings[3]) + (MLR_B_5 * (double)readings[6]) + (MLR_B_6 * (double)readings[7]) + (MLR_B_7 * (double)readings[8]) + (MLR_B_8 * (double)readings[9]);
  if (mlrProduct < 0.0)
  {
    mlrProduct = 0.0;
  }
  Serial.printf("PAR = %f\n", mlrProduct);

  if((millis()-lastPAR)>300000) {
    lastPAR = millis();
    if (mqtt.Update()) {
      mqttPubCh0.publish(readings[0]);
      mqttPubCh1.publish(readings[1]);
      mqttPubCh2.publish(readings[2]);
      mqttPubCh3.publish(readings[3]);
      mqttPubCh4.publish(readings[4]);
      mqttPubCh5.publish(readings[5]);
      mqttPubCh6.publish(readings[6]);
      mqttPubCh7.publish(readings[7]);
      mqttPubCh8.publish(readings[8]);
      mqttPubCh9.publish(readings[9]);
    }
  }

  return mlrProduct;
}

void publishReadings()
{
  MQTT_connect();
  if ((millis() - last) > 120000)
  {
    Serial.printf("Pinging MQTT \n");
    if (!mqtt.ping())
    {
      Serial.printf("Disconnecting \n");
      mqtt.disconnect();
    }
    last = millis();
  }
  if ((millis() - lastTime > 30000))
  {
    if (mqtt.Update())
    {
      mqttPubHydropnicsPARStatus.publish(mlrProduct);
      mqttPubHydropnicsFlowStatus.publish(readWaterFlow());
      mqttPubHydropnicsPHStatus.publish(pHValue);
      mqttPubHydropnicsECStatus.publish(EC);
      Serial.println("Sent the readings to the dashboard.");
    }
    lastTime = millis();
  }
}

void publishWaterLevelReadings()
{
  MQTT_connect();
  if ((millis() - last) > 120000)
  {
    Serial.printf("Pinging MQTT \n");
    if (!mqtt.ping())
    {
      Serial.printf("Disconnecting \n");
      mqtt.disconnect();
    }
    last = millis();
  }
  if ((millis() - lastTime > 30000))
  {
    if (mqtt.Update())
    {
      mqttPubHydropnicsWaterLevelStatus.publish(waterLevelAverage);
      Serial.println("Sent the water level readings to the dashboard.");
      mqttPubHydropnicsWaterLevelPercentStatus.publish((waterLevelAverage / HIGH_WATER_INCHES) * 100.0);
      Serial.println("Sent the water level percent readings to the dashboard.");
    }
    lastTime = millis();
  }
}

// Function to ensure connection to the adafruit dashboard **Credit to Brian Rashap**
void MQTT_connect()
{
  int8_t ret;
  // Stop if already connected.
  if (mqtt.connected())
  {
    return;
  }
  Serial.print("Connecting to MQTT... ");
  while ((ret = mqtt.connect()) != 0)
  { // connect will return 0 for connected
    Serial.printf("%s\n", (char *)mqtt.connectErrorString(ret));
    Serial.printf("Retrying MQTT connection in 5 seconds..\n");
    mqtt.disconnect();
    connectTimer.startTimer(5000);
    while (!connectTimer.isTimerReady())
      ;
  }
  Serial.printf("MQTT Connected!\n");
}

void listenForSubs()
{
  MQTT_connect();
  // this is our 'wait for incoming subscription packets' busy subloop
  Adafruit_MQTT_Subscribe *subscription;
  while ((subscription = mqtt.readSubscription(1000)))
  {
    if (subscription == &mqttSubNutrientPumps)
    {
      int dashNutrientButton = atoi((char *)mqttSubNutrientPumps.lastread);

      if (dashNutrientButton == 1)
      {
        runNutientPump();
      }
    }

    if (subscription == &mqttSubPhUpPump)
    {
      int dashPhUpButton = atoi((char *)mqttSubPhUpPump.lastread);

      if (dashPhUpButton == 1)
      {
        PhUpPumpPinOn();
      }
    }

    if (subscription == &mqttSubPhDownPump)
    {
      int dashPhDownButton = atoi((char *)mqttSubPhDownPump.lastread);

      if (dashPhDownButton == 1)
      {
        PhDownPumpPinOn();
      }
    }

    if (subscription == &mqttSubFreshWaterRefillPump)
    {
      int dashSubFreshWaterRefillButton = atoi((char *)mqttSubPhDownPump.lastread);

      if (dashSubFreshWaterRefillButton == 1)
      {
        freshWaterPumpOn();
      }
    }
  }
}

Credits

Brian Rashap

Brian Rashap

9 projects • 46 followers
Former General Manager of US Facilities Operations at Intel Corporation. Currently loving my encore career as a teacher focused on IoT.
Russell Brazell

Russell Brazell

4 projects • 4 followers
Jessica Rodriquez

Jessica Rodriquez

5 projects • 7 followers

Comments

Add projectSign up / Login