Hardware components | ||||||
| × | 1 | ||||
Software apps and online services | ||||||
![]() |
|
This monitoring system has been designed for use in the aeroponics method of hydroponics, where the crops roots are submerged all the time in an oxygen rich water reservoir.
- Monitor growbed water temperature.
- Monitor growbed nutrient level using a TDS (Total Dissolved Solids Sensor).
- Monitor growbed water levels using an optical level sensor.
- Display growbed data on a 1.3" OLED display for real time feedback.
- Sensors can monitor two different growbeds at the same time.
Four Growbeds with two Growbed Modules daisy chained together for an upper and lower status display.
Finally a third daisy chained Growbed Module on a Deep Water Culture Bucket system.
x1 IO Adder.
x1 4 Port Surface Mount Box White.
x3 Keystone Jack Crossover White.
x1 1.3" I2C 128x64 SSD1306 OLED LCD Display White.
x2 KEYESTUDIO Water Quality TDS Meter Total Dissolved Solids Sensor.
x2 Optical Infrared Liquid Water Level Control Switch.
x2 1M Waterproof DS18B20 Digital Temperature Sensor.
Note: Where you see the 'X' in the phone cable indicates a reverse wiring. Two reverse wires is the same as a straight through wire.
OLED DisplayUsing an exacto knife carefully cut out the OLED screen window in the front of the enclosure.
Position the OLED display on the underside of the enclosure cover and make sure only the screen is visible through the cut out windows. Then secure it use clear packing tape.
Modify the TDS sensor control board by adding an additional 0603 5.6K 1% resistor in parallel to effectively double the reported TDS maximum level to 2000 ppm. Just place it right on top of the existing 5.6k resistor and solder the sides together. By running the 5.6K in parallel you are effectively halving the resistance. 5.6K / 2 = 2.8K.
Lay the two TDS sensor control boards on the bottom of the enclosure. Attach thick single sided tape over the TDS circuit board to insulate it.
Wire up the IO Adder, and connect the RJ11 wire to both sides, laying it on top of the TDS control boards.
Finally close the enclosure carefully, making sure you don't pinch any cables.
To mount the Growbed Module to your shelf use zip tie adhesize-backed mounts.
Now we are ready to install the sensors into the tote. Carefully cut a slot on the side nearest the module using an exacto knife and slide in the temperature and TDS sensor. The water level sensor will be installed in the center of the tote.
Measure 3" from the first bottom lip and drill a 9/16" hole. This will give us about 5 gallons of water when the optical water level sensor will trigger when it reaches the center of the prism. Open up the growbed module, disconnect the sensor from the optical connector and feed it through from the inside of the tote with the silicon washer being on the inside of the tote. On the outside feed the wire through the plastic nut and secure the sensor. Then connect the sensor back to the optical connector.
Make sure that the 3" net cups are not submerged in the water but just above or at the water line. With the air stone bubbling, the net cups should get wet. This way the base of the roots are wet but still exposed to the air.
After you transplant from the germination bed you may need to manually water the seedlings, or fill the tote one inch higher than the optical water level sensor so that it touches the bottom of the net cups, until the root system explodes from the net cups.
Garage Hydroponics
Hydroponics Deep Water Culture Bucket System
Hydroponics Growbed Sensors/Display Module
Hydroponics Chiller
Hydroponics Water/Nutrient Control
Hydroponics Database Management
Hydroponics Germination Control
Hydroponics CO2 Monitoring
Hydroponics Light Monitoring
Hydroponics pH and DO Monitoring
/* 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.
Comments