Christian
Published © LGPL

Professional Hydroponics Growbed Sensor/Display Module

Build a sensor monitoring systems for your Hydroponics Growbed (Using New IO Adder!)

IntermediateFull instructions provided4 hours896

Things used in this project

Hardware components

IO Expander Bundle
×1

Software apps and online services

Arduino IDE
Arduino IDE

Story

Read more

Schematics

Hydroponics Growbed

Code

Professional Garage Hydroponics

C/C++
/* IO Expander
 
   Garage Hydroponics System v1.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.mywebsite.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
#define HUMIDITY_SENSOR_INSIDE  "i6s0e;ic0;st3"  // SHT3x 100kHz w/ 5.1k pullup *** Change 0e
#define HUMIDITY_SENSOR_OUTSIDE "i8s5a;ic0;st3" // SHT3x 100kHz w/ 5.1k pullup *** Change 5a
#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      74.5     
#define HEATER_OFF_DAY_TEMP     75.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   55
#define CHILLER_OFF_WATER_TEMP  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*1)     // 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
 
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[] = "i2sec"; // RJ11 Keystone Crossover Out, T-Connector w/ I2C Bus - OLED Screen/Light Sensor *** Change ec
 
const char TEMP1_SENSOR[] =     "t2r92";    // RJ11 Keystone Crossover In for 1-Wire Junction DS18B20 *** Change 92
const char LEVEL1_SENSOR[] =    "g10i";     // RJ11 Keystone Crossover for Optical Connector
const char TDS1_SENSOR[] =      "g12a";     // RJ11 Plug ADC
#define TDS1_CALIBRATION        (488.0/760.0) // TDS Calibration (Desired/Actual) *** Change
#define WATER1_RELAY            9           // Relay Water Dosing Pump
#define NUTRIENT1_RELAY         10          // 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
const char LEVEL2_SENSOR[] =    "g9i";      // RJ11 Keystone Crossover for Optical Connector
const char TDS2_SENSOR[] =      "g11a";     // RJ11 Plug ADC
#define TDS2_CALIBRATION        (488.0/793.0) // TDS Calibration (Desired/Actual) *** Change
#define WATER2_RELAY            11          // Relay Water Dosing Pump
#define NUTRIENT2_RELAY         12          // Relay Nutrient Dosing Pump
#define CHILLER2_RELAY          16          // Relay Chiller Solenoid
 
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[] = "i2s4a";   // I2C BUS - CO2 Sensor *** Change 4a
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[] = "i2s56";    // 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[] = "i2s5d";    // 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_sensor;
  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;
  uint16_t water_tds;
  bool water_pump;
  bool nutrient_pump;
  float nutrient_level;
  bool chiller_solenoid;
} GROWBED_t;
 
GROWBED_t grow_bed_table[] = {
  {true, // Left
   ONEWIRE_TO_I2C_GROW1,
   TEMP1_SENSOR,
   LEVEL1_SENSOR,
   TDS1_SENSOR,
   WATER1_RELAY,
   NUTRIENT1_RELAY,
   CHILLER1_RELAY,
   TDS1_CALIBRATION,
   true,
   0.0,
   false,
   false,
   0,
   false,
   false,
   488.0,
   false},
  {true, // Right
   ONEWIRE_TO_I2C_GROW1,
   TEMP2_SENSOR,
   LEVEL2_SENSOR,
   TDS2_SENSOR,
   WATER2_RELAY,
   NUTRIENT2_RELAY,
   CHILLER2_RELAY,
   TDS2_CALIBRATION,
   true,
   0.0,
   false,
   false,
   0,
   false,
   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) + 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 + temp);
  float l = log(humidity / 100);
  float b = l + 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 + 243.5)) * relative * MOLAR_MASS_OF_WATER) / ((273.15 + 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] += (watts * 100) / MIN_IN_HOUR;
  power += 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);
  }
}
 
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 " + 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;
  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) + 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 + (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)) {
            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 + g + b > c) ? (r + g + 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 +                   /** Red coefficient. */
                    1.000f * (float)g2 +                   /** 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 + 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))
        {
          if (SerialCmdNoError(PH_SENSOR)) {
            delay(900);
            SerialCmd("ia");
            SerialReadInt(&rc);
            if (rc == 1) SerialReadFloat(&pH);
            SerialReadUntilDone();
            SerialCmdDone(PH_SLEEP);
          }
        }
        // Check for Atlas Scientific DO probe
        DO = -1;          
        if (SerialCmdNoError(ONEWIRE_TO_I2C_DO)) 
        {
          if (SerialCmdNoError(DO_SENSOR)) {
            delay(600);
            SerialCmd("ia");
            SerialReadInt(&rc);
            if (rc == 1) SerialReadFloat(&DO);
            SerialReadUntilDone();
            SerialCmdDone(DO_SLEEP);
          }
        }
 
        // Update Grow Beds
        water_pump = false;
        grow_bed = grow_bed_table;
        for (i = 0; i < sizeof(grow_bed_table) / sizeof(GROWBED_t); i++) {
 
          SerialCmd(grow_bed->level_sensor);
          if (SerialReadInt(&level)) {
            grow_bed->water_level = (level == 0);
          }
          SerialReadUntilDone();
 
          // Check the water temperature
          SerialCmd(grow_bed->temp_sensor);
          grow_bed->water_temp_error = !SerialReadFloat(&grow_bed->water_temp);
          SerialReadUntilDone();
 
          if (!grow_bed->water_temp_error && C2F(grow_bed->water_temp) < MIN_WATER_TEMP)
            heater = true;
 
          // Check TDS sensor
          grow_bed->water_tds = -1;
          SerialCmd(grow_bed->tds_sensor);
          if (SerialReadFloat(&voltage) &&
              SerialReadFloat(&vref)) {
            // Caculate the temperature copensated voltage
            voltage /= 1.0 + 0.02 * (grow_bed->water_temp - 25.0);
            // TDS sensor doubling measurment add 5.6K additional resistor in parallel at R10 (* 2)
            // 0.5 is the recommended conversion factor based upon sodium chloride solution.
            // Use 0.65 and 0.70 for an estimated conversion factor if there are salts present in the fertilizer that do not dissociate.
            // Use 0.55 for potassium chloride.
            // Use 0.70 for natural mineral salts in fresh water - wells, rivers, lakes.
            grow_bed->water_tds = ((133.42 * voltage * voltage * voltage - 255.86 * voltage * voltage + 857.39 * voltage) * 0.5) * 2 * grow_bed->tds_calibration;
          }
          SerialReadUntilDone();
 
          // Check dosing pumps.  Allow for a one minute mixing cycle between nutrient pumps.
          if (!grow_bed->active || grow_bed->water_level || grow_bed->nutrient_pump) {
            grow_bed->water_pump = false;
            grow_bed->nutrient_pump = false;
          }
          else {
            bool nutrient_pump = (grow_bed->water_tds < grow_bed->nutrient_level) ? true : false;
            grow_bed->water_pump = !nutrient_pump;
            grow_bed->nutrient_pump = nutrient_pump;
          }
          sprintf(cmd, "e%d%c;e%d%c", grow_bed->water_relay, (grow_bed->water_pump) ? 'o' : 'f', grow_bed->nutrient_relay, (grow_bed->nutrient_pump) ? 'o' : 'f');
          SerialCmdDone(cmd);
          if (grow_bed->water_pump) AddPower(DOSING_PUMP_POWER);
          if (grow_bed->nutrient_pump) AddPower(DOSING_PUMP_POWER);
 
          // Check chiller pumps
          if (grow_bed->chiller_relay) {
            if (grow_bed->active &&
              chiller >= 0 &&
              chiller_recovery_time < CHILLER_RECOVERY_TIME && 
              C2F(chiller_temp) < SOLENOID_OFF_WATER_TEMP) {
              if (grow_bed->water_temp_error) grow_bed->chiller_solenoid = false;
              else {
                if (grow_bed->chiller_solenoid) {
                  if (C2F(grow_bed->water_temp) <= SOLENOID_OFF_WATER_TEMP) grow_bed->chiller_solenoid = false;
                }
                else {
                  if (C2F(grow_bed->water_temp) >= SOLENOID_ON_WATER_TEMP) grow_bed->chiller_solenoid = true;
                }
              }
            }
            else grow_bed->chiller_solenoid = false;  
            Serial.print("e");
            Serial.print(grow_bed->chiller_relay);
            SerialCmdDone((grow_bed->chiller_solenoid) ? "o" : "f");
            if (grow_bed->chiller_solenoid) {
              water_pump = true;
              AddPower(CHILLER_SOLENOID_POWER);
              delay(900); // Add additional delay for current in rush to the solenoid if powered by the same 12V rail as the IO Expander and x16 Relay module
            }
          }
 
          grow_bed++;
        }
 
        // Calculate Energy Usage
        if (clk.tm_wday != nvram.energy_wday) {
          nvram.energy_wday = clk.tm_wday;
          nvram.energy_usage[nvram.energy_wday] = 0;
          nvram.energy_time[nvram.energy_wday] = 0;
        }
        power = ALWAYS_ON_POWER;
 
        // Turn on/off the lights, fan, heater, heater pad, chiller, and water pump
        ControlRelay(vent_fan, VENT_FAN_ON, VENT_FAN_OFF, VENT_FAN_POWER);
        ControlRelay(lights, LIGHTS_ON, LIGHTS_OFF, LIGHTS_POWER);
        ControlRelay(heater, HEATER_ON, HEATER_OFF, HEATER_POWER);
        ControlRelay(heater_pad, HEATER_PAD_ON, HEATER_PAD_OFF, HEATER_PAD_POWER);
        ControlRelay(chiller, CHILLER_ON, CHILLER_OFF, CHILLER_POWER);
        ControlRelay(water_pump, WATER_PUMP_ON, WATER_PUMP_OFF, WATER_PUMP_POWER);
 
        nvram.energy_time[nvram.energy_wday]++;
 
        // Energy cost is calculated using a weekly weighted scale from 1/7 being last week to today being 7/7.
        energy_usage = energy_time = 0;
        for (i = 1, wday = clk.tm_wday; i <= DAYS_IN_WEEK; i++) {
          if (++wday == DAYS_IN_WEEK) wday = 0;
          energy_usage += (nvram.energy_usage[wday] * i) / DAYS_IN_WEEK;
          energy_time += (nvram.energy_time[wday] * i) / DAYS_IN_WEEK;
        }
        cost = ((float)(energy_usage / energy_time) / 100000.0) * MIN_IN_DAY * (COST_KWH / 100.0);
 
        // Display main status
        if (SerialCmdNoError(ONEWIRE_TO_I2C_MAIN)) {
          if (init_oled) {
            if (SerialCmdNoError(INIT_OLED1) &&
                SerialCmdNoError(INIT_OLED2))
              init_oled = false;
          }
          if (!init_oled) {
            SerialCmdDone("st13;sc;sf0;sa1;sd70,0,\"INSIDE\";sd126,0,\"OUTSIDE\";sf1;sa0;sd0,12,248,\""
#ifdef FAHRENHEIT
                          "F"
#else
                          "C"
#endif
                          "\";sd0,30,\"%\";sf0;sd0,50,\"g/m\";sd20,46,\"3\"");
            SerialPrint("sf1;sa1;sd70,12,\"", C2F(inside.temp), 1, inside.error);
            SerialPrint("\";sd70,30,\"", inside.relative, 1, inside.error);
            SerialPrint("\";sd70,48,\"", inside.absolute, 1, inside.error);
            SerialPrint("\";sd126,12,\"", C2F(outside.temp), 1, outside.error);
            SerialPrint("\";sd126,30,\"", outside.relative, 1, outside.error);
            SerialPrint("\";sd126,48,\"", outside.absolute, 1, outside.error);
            Serial.print("\";sf0;sa0;sd0,0,\"");
            if (vent_fan) Serial.print("FAN");
            else Serial.print("v1.0");
            Serial.println("\"");
            SerialReadUntilDone();
 
            if ((lights && C2F(inside.temp) < MIN_DAY_TEMP) ||
                (!lights && C2F(inside.temp) < MIN_NIGHT_TEMP))
              SerialCmdDone("sh29,11,44;sh29,29,44;sv29,12,17;sv72,12,17");
            else {
              if ((lights && C2F(inside.temp) > MAX_DAY_TEMP) ||
                  (!lights && C2F(inside.temp) > MAX_NIGHT_TEMP))
              SerialCmdDone("so2;sc29,11,44,19;so1");
            }
            if (inside.relative < MIN_HUMIDITY)
              SerialCmdDone("sh29,29,44;sh29,47,44;sv29,30,17;sv72,30,17");
...

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

Credits

Christian
24 projects • 135 followers
Senior Embedded Engineer

Comments