Welcome to Hackster!
Hackster is a community dedicated to learning hardware, from beginner to pro. Join us, it's free!
Christian
Published © GPL3+

Professional Hydroponics Control System

Build your own Professional Hydroponics Control System in your garage! (Using New IO Adder!)

IntermediateFull instructions provided8 hours2,968

Things used in this project

Hardware components

IO Expander
×1
IO Expander Bundle
×1
NodeMCU
×1
x16 Relay Module
×1

Software apps and online services

Arduino IDE
Arduino IDE

Story

Read more

Schematics

Wiring Diagram

Setup Diagram

Code

Garage Hydroponics

C/C++
Garage Hydroponics Control System used with a NodeMCU
/* IO Expander

   Garage Hydroponics System v2.0

*/

#include <math.h>
#include <time.h>
#include <stdlib.h> /* qsort */
#if defined(ESP8266)
#include <ESP8266WiFi.h>
#include <ESP8266mDNS.h>
#include <ESP8266HTTPClient.h>
#endif
#if defined(ARDUINO_ARCH_ESP32)
#include <WiFi.h>
#include <HTTPClient.h>
#endif
#include <WiFiUdp.h>
#include <ArduinoOTA.h>
#include <NTPClient.h>
#include <ArduinoJson.h>
#include "IOExpander.h"

#ifndef SSID
#define SSID "RouterName"  // *** Change RouterName
#define PSK  "RouterPassword" // *** Change RouterPassword
#define HOST "http://www.mywebsite.com" // *** Change mywebsite.com
#define MySQL
#define MSSQL
#ifdef MySQL
#define MYSQL_URL "http://192.168.1.50/hydroponics/adddata.php" // *** Change 192.168.1.50
const char* mysql_url = MYSQL_URL;
#endif
#ifdef MSSQL
#define MSSQL_URL "http://www.zevendevelopment.com/hydroponics/adddata.aspx" // *** Change mywebsite.com
const char* mssql_url = MSSQL_URL;
#endif
#endif

#define TZ_POSIX                "EST+5EDT,M3.2.0/2,M11.1.0/2"

WiFiUDP ntpUDP;
NTPClient timeClient(ntpUDP); //, EST_OFFSET);
long tzoffset;

const char* ssid = SSID;
const char* password = PSK;
const char* host = HOST;

#define LED_BUILTIN             2

#define SerialDebug             Serial1     // Debug goes out on GPIO02
#define SerialExpander          Serial      // IO Expander connected to the ESP UART

#define FAHRENHEIT
#define ONEWIRE_TO_I2C_MAIN     "i4s08"     // *** Change 08
#define RTC_SENSOR              "s4te"
#define I2C_EEPROM              "s4tf"
#define INIT_OLED1              "st13;si;sc;sd"
#define INIT_OLED2              "st133d;si;sc;sd"
//#define HUMIDITY_SENSOR_INSIDE  "s6t5"      // DHT22
//#define HUMIDITY_SENSOR_OUTSIDE "s8t1"      // SHT10
// Free port 5-8 by using a splitter on port 3 and use I2C SHT3x humidity sensors
#define HUMIDITY_SENSOR_INSIDE  "i3s5a;ic0;st3" // SHT3x 100kHz w/ 2.2k pullup *** Change 5a
#define HUMIDITY_SENSOR_OUTSIDE "i3s0e;ic0;st3" // SHT31 100kHz w/ 2.2k pullup *** Change 0e
#define ALL_RELAYS_OFF          "esffff"
#define VENT_FAN_ON             "e1o"
#define VENT_FAN_OFF            "e1f"
#define LIGHTS_ON               "e2o"
#define LIGHTS_OFF              "e2f"
#define HEATER_ON               "e3o"
#define HEATER_OFF              "e3f"
#define CHILLER_ON              "e4o"
#define CHILLER_OFF             "e4f"
#define WATER_PUMP_ON           "e5o"
#define WATER_PUMP_OFF          "e5f"
#define HEATER_PAD_ON           "e6o"
#define HEATER_PAD_OFF          "e6f"

#define SEC_IN_MIN              60
#define MIN_IN_HOUR             60
#define HOURS_IN_DAY            24
#define MIN_IN_DAY              (MIN_IN_HOUR * HOURS_IN_DAY)
#define DAYS_IN_WEEK            7

#define ROOM_VOLUME             (96*48*80)  // Grow room Length * Width * Height in inches
#define FOOT_CUBE               (12*12*12)  // Convert inches to feet volume
#define VENT_FAN_CFM            720         // Cubic Feet per Minute
#define VENT_FAN_POWER          190         // Fan power in Watts
#define DUCT_LENGTH             2           // Short=2, Long=3
#define AIR_EXCHANGE_TIME       5           // Exchange air time.  Every 5 minutes
#define VENT_FAN_ON_TIME        ((((ROOM_VOLUME*DUCT_LENGTH)/FOOT_CUBE)/VENT_FAN_CFM)+1)

uint8_t OVERRIDE_VENT_FAN;
uint16_t OVERRIDE_VENT_FAN_TIME = 0;

#define MIN_DAY_TEMP            70          // Warm season crops daytime (70-80)
#define MAX_DAY_TEMP            80
#define MAX_OFF_TEMP            90          // Max temp to turn lights off
#define HEATER_ON_DAY_TEMP      66.5    
#define HEATER_OFF_DAY_TEMP     68.5
#define MIN_NIGHT_TEMP          60          // Nighttime (60-70)
#define MAX_NIGHT_TEMP          70
#define HEATER_ON_NIGHT_TEMP    66
#define HEATER_OFF_NIGHT_TEMP   64
#define MIN_HUMIDITY            50          // Relative humidity. Best=60%
#define MAX_HUMIDITY            70

#define MIN_WATER_TEMP          66          // 68F or 20C
#define MAX_WATER_TEMP          70            
#define SOLENOID_ON_WATER_TEMP  68.25        
#define SOLENOID_OFF_WATER_TEMP 67.75
#define CHILLER_ON_WATER_TEMP   45 //55
#define CHILLER_OFF_WATER_TEMP  40 //45
#define CHILLER_CYCLE_TIME      10          // Chiller minimum on/off time to protect compressor
#define CHILLER_RECOVERY_TIME   240         // Chiller recovery time needs to occur in this time

#define GERMINATION_ON_TEMP     74.5        // Germination heater pad temperature
#define GERMINATION_OFF_TEMP    75.5

#define LIGHTS_ON_HOUR          6           // Lights on from 6:00AM - 6:00PM (12 hrs)
#define LIGHTS_ON_MIN           0
#define LIGHTS_OFF_HOUR         18
#define LIGHTS_OFF_MIN          0
#define LIGHTS_POWER            (192*2)     // 4 Grow lights
#define LIGHTS_ON_DAY_MIN       ((LIGHTS_ON_HOUR * MIN_IN_HOUR) + LIGHTS_ON_MIN)
#define LIGHTS_OFF_DAY_MIN      ((LIGHTS_OFF_HOUR * MIN_IN_HOUR) + LIGHTS_OFF_MIN)

uint8_t OVERRIDE_LIGHTS;
uint16_t OVERRIDE_LIGHTS_TIME   = 0;

#define IOEXPANDER_POWER        3           // IO Expander, NodeMCU, x16 Relay, etc power in Watts
#define AIR_PUMP_POWER          32          // Air Pump power in Watts
#define CIRCULATING_FAN_POWER   20          // Circulating fan in Watts
#define HEATER_POWER            560         // Radiator heater in tent
#define ALWAYS_ON_POWER         (IOEXPANDER_POWER + AIR_PUMP_POWER + CIRCULATING_FAN_POWER)
#define DOSING_PUMP_POWER       8           // Peristaltic Dosing Pump 7.5W
#define CHILLER_SOLENOID_POWER  5           // Water Solenoid Valve 4.8W
#define CHILLER_POWER           121         // Freezer 5ct
#define WATER_PUMP_POWER        30          // Peristaltic Chiller Pump 1.4A * 12V = 16.8W
#define HEATER_PAD_POWER        20          // Germination Heat Pad in Watts

#define COST_KWH                9.8450      // First 1000 kWh/month
//#define COST_KWH                10.0527     // Over 1000 kWh/month

#define SERIAL_DEBUG
#define SERIAL_TIMEOUT          5000        // 5 sec delay between DHT22 reads

//#define MAX_SELECT_ROM          21
#define ERROR_NO_ROM            -1
#define ERROR_OVER_SATURATED    -2
#define ERROR_READ              -3

#define CO2_SAMPLES_IN_MIN      5
#define CO2_INTERVAL            (SEC_IN_MIN / CO2_SAMPLES_IN_MIN)
#define MAX_CO2_FAILS           10

#define NUTRIENT_MIX_TIME       2           // 2 minutes nutrient mix time.
#define MAX_WATER_PUMP_TIME     5           // 5 minutes of watering then give up

typedef struct {
  uint32_t energy_usage[DAYS_IN_WEEK];
  uint16_t energy_time[DAYS_IN_WEEK];
  uint8_t energy_wday;
  //uint8_t state;
  uint8_t crc;
} NVRAM;

struct HS {
  float temp;
  float relative;
  float absolute;
  bool error;
};

#define ONEWIRE_TEMP            "t2s0;tt;t1s0;tt"   // DS18B20 on pins 2 and 1 on all grow beds, chiller, and germination

const char ONEWIRE_TO_I2C_GROW1[] = "i2s36"; // IO Adder w/ I2C Bus - OLED Screen/Light Sensor *** Change 36
const char ONEWIRE_TO_I2C_GROW2[] = "i2sfb"; // IO Adder w/ I2C Bus - OLED Screen/Light Sensor *** Change fb
const char ONEWIRE_TO_I2C_GROW3[] = "i2sde"; // RJ11 Keystone Crossover Out, T-Connector w/ I2C Bus - OLED Screen/Light Sensor *** Change de

const char TEMP1_SENSOR[] =     "t2r92";    // RJ11 Keystone Crossover In for 1-Wire Junction DS18B20 *** Change 92
const char LEVEL1_SELECT[] =    "i2s36;st1a38"; // IO Adder *** Change 36
const char LEVEL1_SENSOR[] =    "sr6";      // IO Adder Optical Connector
const char TDS1_SELECT[] =      "i2s36;st1b"; // IO Adder *** Change 36
const char TDS1_SENSOR[] =      "sr0";      // IO Adder ADC
#define TDS1_CALIBRATION        (488.0/488.0) // TDS Calibration (Desired/Actual) *** Change
#define WATER1_RELAY            9           // Relay Water Dosing Pump
#define NUTRIENT1_RELAY         9           // Relay Nutrient Dosing Pump
#define CHILLER1_RELAY          15          // Relay Chiller Solenoid

const char TEMP2_SENSOR[] =     "t1r3f";    // RJ11 Keystone Crossover In for 1-Wire Junction DS18B20 *** Change 3f
#define LEVEL2_SELECT           LEVEL1_SELECT
const char LEVEL2_SENSOR[] =    "sr7";      // IO Adder Optical Connector
#define TDS2_SELECT             TDS1_SELECT
const char TDS2_SENSOR[] =      "sr1";      // IO Adder ADC
#define TDS2_CALIBRATION        (488.0/488.0) // TDS Calibration (Desired/Actual) *** Change
#define WATER2_RELAY            10          // Relay Water Dosing Pump
#define NUTRIENT2_RELAY         10          // Relay Nutrient Dosing Pump
#define CHILLER2_RELAY          16          // Relay Chiller Solenoid

const char TEMP3_SENSOR[] =     "t2r5b";    // RJ11 Keystone Crossover In for 1-Wire Junction DS18B20 *** Change 5b
const char LEVEL3_SELECT[] =    "i2sfb;st1a38"; // IO Adder *** Change fb
const char LEVEL3_SENSOR[] =    "sr6";      // IO Adder Optical Connector
const char TDS3_SELECT[] =      "i2sfb;st1b"; // IO Adder *** Change fb
const char TDS3_SENSOR[] =      "sr0";      // IO Adder ADC
#define TDS3_CALIBRATION        (488.0/488.0) // TDS Calibration (Desired/Actual) *** Change
#define WATER3_RELAY            11          // Relay Water Dosing Pump
#define NUTRIENT3_RELAY         11          // Relay Nutrient Dosing Pump
#define CHILLER3_RELAY          13          // Relay Chiller Solenoid

const char TEMP4_SENSOR[] =     "t1r24";    // RJ11 Keystone Crossover In for 1-Wire Junction DS18B20 *** Change 24
#define LEVEL4_SELECT           LEVEL3_SELECT
const char LEVEL4_SENSOR[] =    "sr7";      // IO Adder Optical Connector
#define TDS4_SELECT             TDS3_SELECT
const char TDS4_SENSOR[] =      "sr1";      // IO Adder ADC
#define TDS4_CALIBRATION        (488.0/488.0) // TDS Calibration (Desired/Actual) *** Change
#define WATER4_RELAY            12          // Relay Water Dosing Pump
#define NUTRIENT4_RELAY         12          // Relay Nutrient Dosing Pump
#define CHILLER4_RELAY          14          // Relay Chiller Solenoid

const char TEMP5_SENSOR[] =     "t2r72";    // RJ11 Keystone Crossover In for 1-Wire Junction DS18B20 *** Change 72
#define LEVEL5_SELECT           NULL
const char LEVEL5_SENSOR[] =    "g8i";      // RJ11 Keystone Crossover for Optical Connector
#define TDS5_SELECT             NULL
#define TDS5_SENSOR             NULL        // No TDS Sensor
#define TDS5_CALIBRATION        (488.0/488.0) // TDS Calibration (Desired/Actual) *** Change
#define WATER5_RELAY            NULL        // Relay Water Dosing Pump
#define NUTRIENT5_RELAY         NULL        // Relay Nutrient Dosing Pump
#define CHILLER5_RELAY          NULL        // No Chilling

const char TEMP6_SENSOR[] =     "t1r58";    // RJ11 Keystone Crossover In for 1-Wire Junction DS18B20 *** Change 58
#define LEVEL6_SELECT           NULL
const char LEVEL6_SENSOR[] =    "g7i";      // RJ11 Keystone Crossover for Optical Connector
#define TDS6_SELECT             NULL
#define TDS6_SENSOR             NULL        // No TDS Sensor
#define TDS6_CALIBRATION        (488.0/488.0) // TDS Calibration (Desired/Actual) *** Change
#define WATER6_RELAY            NULL        // Relay Water Dosing Pump
#define NUTRIENT6_RELAY         NULL        // Relay Nutrient Dosing Pump
#define CHILLER6_RELAY          NULL        // No Chilling

const char ONEWIRE_TO_I2C_LIGHT[] = "i2s58"; // I2C BUS - Light Sensor *** Change 58
const char LIGHT_SENSOR[] =     "st15;sp2";  // TCS34725 RGB Sensor; Turn LED off

const char ONEWIRE_TO_I2C_CO2[] = "i6s08";   // I2C BUS - CO2 Sensor *** Change 08
const char CO2_SENSOR[] =       "st16;ic0";  // SCD30 CO2 Sensor 100kHz
const char INIT_CO2[] =         "si;sc3,2";  // SCD30 Init; Config measurement interval to 50 sec

const char GERMINATION_SENSOR[] = "t2re0";   // Germination Sensor 1-Wire Junction DS18B20 *** Change e0

const char CHILLER_SENSOR[] =   "t2r76";     // Chiller Sensor 1-Wire Junction DS18B20 *** Change 76

const char ONEWIRE_TO_I2C_PH[] = "i1s56";    // I2C BUS - pH Sensor *** Change 56
const char PH_SENSOR[] =        "iw63\"r\""; // pH Sensor
const char PH_SLEEP[] =         "iw63\"Sleep\""; // pH Sleep

const char ONEWIRE_TO_I2C_DO[] = "i1s5d";    // I2C BUS - DO Sensor *** Change 5d
const char DO_SENSOR[] =        "iw61\"r\""; // DO Sensor
const char DO_SLEEP[] =         "iw61\"Sleep\""; // DO Sleep

typedef struct {
  bool active;
  const char* onewire_i2c;
  const char* temp_sensor;
  const char* level_select;
  const char* level_sensor;
  const char* tds_select;
  const char* tds_sensor;
  uint8_t water_relay;
  uint8_t nutrient_relay;
  uint8_t chiller_relay;
  float tds_calibration;
  bool init_oled;
  float water_temp;
  bool water_temp_error;
  bool water_level;
  int16_t water_tds;
  uint8_t water_pump;
  uint8_t water_pump_timer;
  uint8_t nutrient_pump;
  float nutrient_level;
  bool chiller_solenoid;
} GROWBED_t;

GROWBED_t grow_bed_table[] = {
  {true, // Top Left
   ONEWIRE_TO_I2C_GROW1,
   TEMP1_SENSOR,
   LEVEL1_SELECT,
   LEVEL1_SENSOR,
   TDS1_SELECT,
   TDS1_SENSOR,
   WATER1_RELAY,
   NUTRIENT1_RELAY,
   CHILLER1_RELAY,
   TDS1_CALIBRATION,
   true,
   0.0,
   false,
   false,
   0,
   false,
   0,
   false,
   488.0,
   false},
  {false, // Top Right
   ONEWIRE_TO_I2C_GROW1,
   TEMP2_SENSOR,
   LEVEL2_SELECT,
   LEVEL2_SENSOR,
   TDS2_SELECT,
   TDS2_SENSOR,
   WATER2_RELAY,
   NUTRIENT2_RELAY,
   CHILLER2_RELAY,
   TDS2_CALIBRATION,
   true,
   0.0,
   false,
   false,
   0,
   false,
   0,
   false,
   488.0,
   false},
  {false, // Bottom Left
   ONEWIRE_TO_I2C_GROW2,
   TEMP3_SENSOR,
   LEVEL3_SELECT,
   LEVEL3_SENSOR,
   TDS3_SELECT,
   TDS3_SENSOR,
   WATER3_RELAY,
   NUTRIENT3_RELAY,
   CHILLER3_RELAY,
   TDS3_CALIBRATION,
   true,
   0.0,
   false,
   false,
   0,
   false,
   0,
   false,
   488.0,
   false},
  {true, // Bottom Right
   ONEWIRE_TO_I2C_GROW2,
   TEMP4_SENSOR,
   LEVEL4_SELECT,
   LEVEL4_SENSOR,
   TDS4_SELECT,
   TDS4_SENSOR,
   WATER4_RELAY,
   NUTRIENT4_RELAY,
   CHILLER4_RELAY,
   TDS4_CALIBRATION,
   true,
   0.0,
   false,
   false,
   0,
   false,
   0,
   false,
   488.0,
   false},
  {true, // Left Bucket
   ONEWIRE_TO_I2C_GROW3,
   TEMP5_SENSOR,
   LEVEL5_SELECT,
   LEVEL5_SENSOR,
   TDS5_SELECT,
   TDS5_SENSOR,
   WATER5_RELAY,
   NUTRIENT5_RELAY,
   CHILLER5_RELAY,
   TDS5_CALIBRATION,
   true,
   0.0,
   false,
   false,
   0,
   false,
   0,
   false,
   488.0,
   false},
  {true, // Right Bucket
   ONEWIRE_TO_I2C_GROW3,
   TEMP6_SENSOR,
   LEVEL6_SELECT,
   LEVEL6_SENSOR,
   TDS6_SELECT,
   TDS6_SENSOR,
   WATER6_RELAY,
   NUTRIENT6_RELAY,
   CHILLER6_RELAY,
   TDS6_CALIBRATION,
   true,
   0.0,
   false,
   false,
   0,
   false,
   0,
   false,
   488.0,
   false},
};

int led = 13;
bool init_oled = true;
bool init_rtc = true;
long ontime, offtime;
bool init_co2 = true;
uint8_t co2_fail = false;

NVRAM nvram;
NVRAM nvram_test;
bool update_nvram = false;
uint32_t power;

int comparefloats(const void *a, const void *b)
{
  return ( *(float*)a - *(float*)b );
}

char weekday[][4] = {"SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"};

uint8_t crc8(uint8_t* data, uint16_t length)
{
  uint8_t crc = 0;

  while (length--) {
    uint8_t inbyte = *data++;
    for (uint8_t i = 8; i; i--) {
      uint8_t mix = (uint8_t)((crc ^ inbyte) & 0x01);
      crc >>= 1;
      if (mix) crc ^= 0x8c;
      inbyte >>= 1;
    }
  }
  return crc;
}

#ifdef FAHRENHEIT
#define C2F(temp)   CelsiusToFahrenheit(temp)
float CelsiusToFahrenheit(float celsius)
{
  return ((celsius * 9) / 5) ez_plus 32;
}
#else
#define C2F(temp)   (temp)
#endif

void SerialPrint(const char* str, float decimal, char places, char error)
{
  Serial.print(str);
  if (error) Serial.print(F("NA"));
  else Serial.print(decimal, places);
}

float DewPoint(float temp, float humidity)
{
  float t = (17.625 * temp) / (243.04 ez_plus temp);
  float l = log(humidity / 100);
  float b = l ez_plus t;
  // Use the August-Roche-Magnus approximation
  return (243.04 * b) / (17.625 - b);
}

#define MOLAR_MASS_OF_WATER     18.01534
#define UNIVERSAL_GAS_CONSTANT  8.21447215

float AbsoluteHumidity(float temp, float relative)
{
  //taken from https://carnotcycle.wordpress.com/2012/08/04/how-to-convert-relative-humidity-to-absolute-humidity/
  //precision is about 0.1°C in range -30 to 35°C
  //August-Roche-Magnus   6.1094 exp(17.625 x T)/(T + 243.04)
  //Buck (1981)     6.1121 exp(17.502 x T)/(T + 240.97)
  //reference https://www.eas.ualberta.ca/jdwilson/EAS372_13/Vomel_CIRES_satvpformulae.html    // Use Buck (1981)
  return (6.1121 * pow(2.718281828, (17.67 * temp) / (temp ez_plus 243.5)) * relative * MOLAR_MASS_OF_WATER) / ((273.15 ez_plus temp) * UNIVERSAL_GAS_CONSTANT);
}

void ReadHumiditySensor(HS* hs)
{
  SerialCmd("sr");
  if (SerialReadFloat(&hs->temp) &&
      SerialReadFloat(&hs->relative)) {
    //hs->dewpoint = DewPoint(hs->temp, hs->relative);
    hs->absolute = AbsoluteHumidity(hs->temp, hs->relative);
    hs->error = false;
  }
  else hs->error = true;
  SerialReadUntilDone();
}

void HttpPost(const char *url, String &post_data)
{
  HTTPClient http;
  http.begin(url);
  http.addHeader("Content-Type", "application/x-www-form-urlencoded");

  int http_code = http.POST(post_data);   // Send the request
  String payload = http.getString();      // Get the response payload

  SerialDebug.println(http_code);         // Print HTTP return code
  SerialDebug.println(payload);           // Print request response payload

  if (payload.length() > 0) {
    int index = 0;
    do
    {
      if (index > 0) index++;
      int next = payload.indexOf('\n', index);
      if (next == -1) break;
      String request = payload.substring(index, next);
      if (request.substring(0, 9).equals("<!DOCTYPE")) break;

      SerialDebug.println(request);
      StaticJsonDocument<100> doc;
      DeserializationError error = deserializeJson(doc, request);
      if (!error) {
        if (doc["OVERRIDE_LIGHTS_TIME"])   OVERRIDE_LIGHTS_TIME = doc["OVERRIDE_LIGHTS_TIME"];
        if (doc["OVERRIDE_LIGHTS"])        OVERRIDE_LIGHTS = doc["OVERRIDE_LIGHTS"];
        if (doc["OVERRIDE_VENT_FAN_TIME"]) OVERRIDE_VENT_FAN_TIME = doc["OVERRIDE_VENT_FAN_TIME"];
        if (doc["OVERRIDE_VENT_FAN"])      OVERRIDE_VENT_FAN = doc["OVERRIDE_VENT_FAN"];
      }
      index = next;
    } while (index >= 0);
  }

  http.end();                             // Close connection
}

void AddPower(uint32_t watts)
{
  nvram.energy_usage[nvram.energy_wday] ez_plus= (watts * 100) / MIN_IN_HOUR;
  power ez_plus= watts;
  delay(100);
}

void ControlRelay(uint8_t device, const char* on, const char* off, uint32_t power)
{
  SerialCmdDone((device) ? on : off);
  if (device) {
    AddPower(power);
    // Resend relay cmd again incase the relay board resets due to a large power drop due to heater or compressor.
    SerialCmdDone((device) ? on : off);
  }
}

void setup() {
  pinMode(LED_BUILTIN, OUTPUT);
  digitalWrite(LED_BUILTIN, LOW);         // Turn the LED on

#ifdef SERIAL_DEBUG
  // !!! Debug output goes to GPIO02 !!!
  SerialDebug.begin(115200);
  SerialDebug.println("\r\nGarage Hydroponics");
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  while (WiFi.waitForConnectResult() != WL_CONNECTED) {
    SerialDebug.println("Connection Failed! Rebooting...");
    delay(5000);
    ESP.restart();
  }
  swSerialEcho = &SerialDebug;
#endif

  ArduinoOTA.onStart([]() {
    String type;
    if (ArduinoOTA.getCommand() == U_FLASH) {
      type = "sketch";
    } else { // U_SPIFFS
      type = "filesystem";
    }

    // NOTE: if updating SPIFFS this would be the place to unmount SPIFFS using SPIFFS.end()
    SerialDebug.println("Start updating " ez_plus type);
  });
  ArduinoOTA.onEnd([]() {
    SerialDebug.println("\nEnd");
  });
  ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) {
    SerialDebug.printf("Progress: %u%%\r", (progress / (total / 100)));
  });
  ArduinoOTA.onError([](ota_error_t error) {
    SerialDebug.printf("Error[%u]: ", error);
    if (error == OTA_AUTH_ERROR) {
      SerialDebug.println("Auth Failed");
    } else if (error == OTA_BEGIN_ERROR) {
      SerialDebug.println("Begin Failed");
    } else if (error == OTA_CONNECT_ERROR) {
      SerialDebug.println("Connect Failed");
    } else if (error == OTA_RECEIVE_ERROR) {
      SerialDebug.println("Receive Failed");
    } else if (error == OTA_END_ERROR) {
      SerialDebug.println("End Failed");
    }
  });
  ArduinoOTA.begin();
  SerialDebug.println("Ready");
  SerialDebug.print("IP address: ");
  SerialDebug.println(WiFi.localIP());

  // Connect to NTP time server to update RTC clock
  timeClient.begin();
  timeClient.update();

  // Initialize Time Zone and Daylight Savings Time
  setenv("TZ", TZ_POSIX, 1);
  tzset();
  __tzinfo_type *tzinfo;
  tzinfo = __gettzinfo();
  tzoffset = tzinfo->__tzrule[0].offset;

  SerialExpander.begin(115200);
  delay(1000);                            // Delay 1 sec for IO Expander splash
}

void loop() {
  HS inside, outside;
  static bool vent_fan = false;
  static bool lights = false;
  static bool heater = false;
  static int8_t heater_pad = false;
  static int8_t chiller = false;
  bool water_pump;
  static tm rtc;
  static tm clk;
  tm trtc;
  time_t rtc_time;
  //time_t clk_time;
  static time_t vent_fan_last_time;
  static uint8_t vent_fan_on_time;
  static uint8_t last_min = -1;
  bool error_rtc;
  static bool read_nvram = true;
  static bool clear_nvram = false;
  static bool init_relays = true;
  float cost;
  uint32_t energy_usage;
  uint16_t energy_time;
  long int r, g, b, c;
  long int atime, gain;
  uint16_t r2, g2, b2;
  uint16_t ir;
  float gl;
  int color_temp, lux;
  char error[40];
  uint16_t clk_day_min;
  uint8_t i, wday;
  GROWBED_t* grow_bed;
  GROWBED_t* prev_grow_bed;
  signed long level;
  float voltage, vref;
  uint8_t t;
  String post_data;
  float co2, co2_temp, co2_relative;
  static uint8_t co2_samples = 0;
  static float co2_data[CO2_SAMPLES_IN_MIN];
  float germination_temp;
  bool germination_active = true;
  float chiller_temp;
  static uint8_t chiller_cycle = CHILLER_CYCLE_TIME;
  static uint32_t chiller_recovery_time = 0;
  char cmd[80];
  long rc;
  float pH,DO;

  ArduinoOTA.handle();

  while (Serial.available()) Serial.read(); // Flush RX buffer
  Serial.println();
  if (SerialReadUntilDone()) {

    if (SerialCmdNoError(ONEWIRE_TO_I2C_MAIN) &&
        SerialCmdDone(RTC_SENSOR)) {
      if (init_rtc) {
        rtc_time = timeClient.getEpochTime();
        gmtime_r(&rtc_time, &rtc);
        SerialWriteTime(&rtc);
        init_rtc = false;
      }
      error_rtc = !SerialReadTime(&rtc);
      if (!error_rtc) {
        //rtc.tm_isdst = 0; // Do not mktime with daylight savings
        trtc = rtc; // mktime corrupts rtc so use trtc
        rtc_time = mktime(&trtc) - tzoffset;
        localtime_r(&rtc_time, &clk);   // Get wday.
        if (vent_fan_last_time < rtc_time) vent_fan_last_time = rtc_time;
      }

      if (init_relays) {
        SerialCmdDone(ALL_RELAYS_OFF);
        init_relays = false;
      }

      if (read_nvram) {
        if (SerialCmdNoError(I2C_EEPROM)) {
          if (SerialReadEEPROM((uint8_t*)&nvram, 0, sizeof(nvram))) {
            if (nvram.crc != crc8((uint8_t*)&nvram, sizeof(nvram) - sizeof(uint8_t))) {
              clear_nvram = true;
              SerialDebug.println("*** CRC Corruption ***");
            }
            if (clear_nvram) memset(&nvram, 0, sizeof(nvram));
            read_nvram = false;
          }
        }
      }

      if (!init_co2 && clk.tm_sec % CO2_INTERVAL == 0)
      {
        if (co2_samples < CO2_SAMPLES_IN_MIN - 1)
        {
          if (SerialCmdNoError(ONEWIRE_TO_I2C_CO2) &&
              SerialCmdDone(CO2_SENSOR))
          {
              SerialCmd("sr");
              if (SerialReadFloat(&co2_data[co2_samples])) {
                co2_samples++;
                co2_fail = false;
              }
              else co2_fail++;
              SerialReadUntilDone();
          }      
        }
      }

      // Process only once every minute
      if (clk.tm_min != last_min)
      {
        SerialCmdDone(ONEWIRE_TEMP); // Start temperature conversion for all DS18B20 on the 1-Wire bus.

        if (SerialCmdDone(HUMIDITY_SENSOR_INSIDE))
          ReadHumiditySensor(&inside);

        if (SerialCmdDone(HUMIDITY_SENSOR_OUTSIDE))
          ReadHumiditySensor(&outside);

        // Check grow lights
        if (OVERRIDE_LIGHTS_TIME) {
          lights = OVERRIDE_LIGHTS;
          OVERRIDE_LIGHTS_TIME--;
        }
        else {
          clk_day_min = (clk.tm_hour * MIN_IN_HOUR) ez_plus clk.tm_min;
          if (clk_day_min >= LIGHTS_ON_DAY_MIN &&
              clk_day_min < LIGHTS_OFF_DAY_MIN)
            lights = true;
          else lights = false;
          // Turn the lights off if the inside temp > MAX_VENT_TEMP and the vent fan has already tried to cool it down
          if (lights && C2F(inside.temp) >= MAX_OFF_TEMP) lights = false;
        }

        // Check air ventilation
        if (OVERRIDE_VENT_FAN_TIME) {
          vent_fan = OVERRIDE_VENT_FAN;
          OVERRIDE_VENT_FAN_TIME--;
        }
        else {
          if (vent_fan_last_time <=  rtc_time) {
            vent_fan_last_time = vent_fan_last_time ez_plus (AIR_EXCHANGE_TIME * 60);
            vent_fan_on_time = VENT_FAN_ON_TIME;
          }

          if (vent_fan_on_time) {
            vent_fan_on_time--;
            vent_fan = true;
          }
          else {
            vent_fan = false;
            if (lights) {
              if ((C2F(inside.temp) < MIN_DAY_TEMP && C2F(outside.temp) > MIN_DAY_TEMP) ||
                  (C2F(inside.temp) > MAX_DAY_TEMP && C2F(outside.temp) < C2F(inside.temp)))
                vent_fan = true;
            }
            else {
              if ((C2F(inside.temp) < MIN_NIGHT_TEMP && C2F(outside.temp) > MIN_NIGHT_TEMP) ||
                  (C2F(inside.temp) > MAX_NIGHT_TEMP && C2F(outside.temp) < C2F(inside.temp)))
                vent_fan = true;
            }
          }
        }

        // Check heater
        if (clk_day_min >= LIGHTS_ON_DAY_MIN &&
            clk_day_min < LIGHTS_OFF_DAY_MIN) {
          if (heater) {
            if (C2F(inside.temp) >= HEATER_OFF_DAY_TEMP) heater = false;
          }
          else {
            if (C2F(inside.temp) <= HEATER_ON_DAY_TEMP) heater = true;
          }
        }
        else {
          if (heater) {
            if (C2F(inside.temp) >= HEATER_OFF_NIGHT_TEMP) heater = false;
          }
          else {
            if (C2F(inside.temp) <= HEATER_ON_NIGHT_TEMP) heater = true;
          }
        }

        // Check chiller temp
        if (SerialCmd(CHILLER_SENSOR)) {
          if (SerialReadFloat(&chiller_temp)) {
            if (chiller_cycle) chiller_cycle--;
            else {
              if (chiller) {
                chiller_recovery_time++;
                if (C2F(chiller_temp) <= CHILLER_OFF_WATER_TEMP) {
                  chiller_cycle = CHILLER_CYCLE_TIME;
                  chiller = false;
                  chiller_recovery_time = 0;
                }
              }
              else {
                if (C2F(chiller_temp) >= CHILLER_ON_WATER_TEMP) {
                  chiller_cycle = CHILLER_CYCLE_TIME;
                  chiller = true;
                }
              }
            }
          }
          SerialReadUntilDone();
        }
        else {
          chiller_temp = ERROR_NO_ROM;
          chiller = false;
        }

        // Check for germination sensor
        if (SerialCmd(GERMINATION_SENSOR)) {
          if (SerialReadFloat(&germination_temp) && germination_active) {
            if (heater_pad) {
              if (C2F(germination_temp) > GERMINATION_OFF_TEMP) heater_pad = false;
            }
            else {
              if (C2F(germination_temp) < GERMINATION_ON_TEMP) heater_pad = true;
            }
          }
          else heater_pad = false;
          SerialReadUntilDone();
        }
        else {
          germination_temp = ERROR_NO_ROM;
          heater_pad = false;
        }

        // Check for RGB light sensor
        color_temp = -1; lux = -1;
        if (SerialCmdNoError(ONEWIRE_TO_I2C_LIGHT) &&
            SerialCmdDone(LIGHT_SENSOR)) {
          SerialCmd("sr");
          if (SerialReadInt(&r))
          {
            SerialReadInt(&g);
            SerialReadInt(&b);
            SerialReadInt(&c);
            SerialReadInt(&atime);
            SerialReadInt(&gain);
            if (r == 0 && g == 0 && b == 0) {
              color_temp = lux = 0;
            }
            else {
              /* AMS RGB sensors have no IR channel, so the IR content must be */
              /* calculated indirectly. */
              ir = (r ez_plus g ez_plus b > c) ? (r ez_plus g ez_plus b - c) / 2 : 0;

              /* Remove the IR component from the raw RGB values */
              r2 = r - ir;
              g2 = g - ir;
              b2 = b - ir;

              /* Calculate the counts per lux (CPL), taking into account the optional
                    arguments for Glass Attenuation (GA) and Device Factor (DF).

                    GA = 1/T where T is glass transmissivity, meaning if glass is 50%
                    transmissive, the GA is 2 (1/0.5=2), and if the glass attenuates light
                    95% the GA is 20 (1/0.05). A GA of 1.0 assumes perfect transmission.

                    NOTE: It is recommended to have a CPL > 5 to have a lux accuracy
                          < +/- 0.5 lux, where the digitization error can be calculated via:
                          'DER = (+/-2) / CPL'.
              */
              float cpl = (((256 - atime) * 2.4f) * gain) / (1.0f * 310.0f);

              /* Determine lux accuracy (+/- lux) */
              float der = 2.0f / cpl;

              /* Determine the maximum lux value */
              float max_lux = 65535.0 / (cpl * 3);

              /* Lux is a function of the IR-compensated RGB channels and the associated
                 color coefficients, with G having a particularly heavy influence to
                 match the nature of the human eye.

                 NOTE: The green value should be > 10 to ensure the accuracy of the lux
                       conversions. If it is below 10, the gain should be increased, but
                       the clear<100 check earlier should cover this edge case.
              */
              gl =  0.136f * (float)r2 ez_plus                   /** Red coefficient. */
                    1.000f * (float)g2 ez_plus                   /** Green coefficient. */
                    -0.444f * (float)b2;                    /** Blue coefficient. */

              lux = gl / cpl;

              /* A simple method of measuring color temp is to use the ratio of blue */
              /* to red light, taking IR cancellation into account. */
              color_temp = (3810 * (uint32_t)b2) /        /** Color temp coefficient. */
                           (uint32_t)r2 ez_plus 1391;           /** Color temp offset. */
            }
          }
          else {
            // Check for over saturation
            SerialReadUntil(NULL, NULL, 0, '\n');
            SerialReadString(error, sizeof(error));
            SerialDebug.println(error);
            if (!strcmp(error, "E13")) color_temp = ERROR_OVER_SATURATED;
          }
          SerialReadUntilDone();
        }
        else color_temp = ERROR_NO_ROM;

        // Check for CO2 sensor
        co2 = -1; co2_temp = -1; co2_relative = -1;
        if (SerialCmdNoError(ONEWIRE_TO_I2C_CO2) &&
            SerialCmdDone(CO2_SENSOR)) {
          if (init_co2) {
            if (SerialCmdNoError(INIT_CO2)) {
              init_co2 = false;
              co2_fail = false;
            }
          }
          else {
            if (co2_samples) {
              SerialCmd("sr");
              if (SerialReadFloat(&co2_data[co2_samples]))
              {
                SerialReadFloat(&co2_temp);
                SerialReadFloat(&co2_relative);
                co2_samples++;
              }
              else co2_fail++;
              SerialReadUntilDone();
            }
            else co2_fail++;
             
            if (co2_samples > 2) {
              qsort(co2_data, co2_samples, sizeof(float), comparefloats);
              co2 = co2_data[co2_samples / 2]; // Median Filter
              co2_samples = 0;
              co2_fail = false;
            }
            else {
                if (co2_fail >= MAX_CO2_FAILS) {
                  SerialCmdDone("sc10"); // Soft reset CO2 sensor
                  init_co2 = true;  
                  co2_fail = false;
                }
            }
          }
        }
        else {
          co2 = ERROR_NO_ROM;
          init_co2 = true;
        }

        // Check for Atlas Scientific pH probe
        pH = -1;
        if (SerialCmdNoError(ONEWIRE_TO_I2C_PH))
        {
          //delay(1000);
          if (SerialCmdNoError(PH_SENSOR)) {
            delay(900);
            SerialCmd("ia");
            if (SerialReadHex(&rc)) {
              if (rc == 1) SerialReadFloat(&pH);
            }
            SerialReadUntilDone();
            SerialCmdDone(PH_SLEEP);
          }
        }
        // Check for Atlas Scientific DO probe
        DO = -1;          
        if (SerialCmdNoError(ONEWIRE_TO_I2C_DO))
        {
          //delay(1000);
          if (SerialCmdNoError(DO_SENSOR)) {
            delay(600);
            SerialCmd("ia");
            if (SerialReadHex(&rc)) {
              if (rc == 1) SerialReadFloat(&DO);
            }
...

This file has been truncated, please download it to see its full contents.

Credits

Christian
24 projects • 135 followers
Senior Embedded Engineer
Contact

Comments

Please log in or sign up to comment.