4 周后,它们几乎可以收割了!
首先在车库中找到一个漂亮的空地,让您可以进入水培系统的各个方面。
大多数车库不加热和冷却,所以为了帮助保持一个更稳定的环境,绝缘是你的朋友。车库最明显和强制性的部分是首先绝缘是车库门。使用可以在当地五金店买到的绝缘材料。我选择了 Rmax R-Matte Plus-3 3/4",4 英尺 x 8 英尺的床单,我使用精确刀切割成合适的尺寸。然后将合适的尺寸水平切成两半,并压缩到车库门槽中,确保箔侧面向外面有半英寸的气隙。这个气隙给了我相当于 6 的总 R 系数。绝缘越好,你以后支付的加热和冷却费用就越少。
x6 Rmax R-Matte Plus-3 3/4" x 4' x 8'. R-5 聚异氰脲酸酯硬质泡沫绝缘板
注意:确保密封车库中允许室外空气进入的所有区域。
由于您将加热、冷却和提供人造植物光,因此建议为您的水培系统添加专用断路器。让有执照的电工为您添加一个新的 20 安培 GFI 断路器。大多数断路器都在车库中,因此添加新电路应该是一种成本相对较低的选择,以实现更好的隔离和安全。
在车库的空地上搭建您的水培帐篷。
x1 VIVOSUN 96"x48"x80" 聚酯薄膜水培种植帐篷房
x1 VIVOSUN 8" 直列管道风扇,带 Speeder 空气碳过滤器管道组合
x1 Quantum Storage 4 层线架单元,300 磅负载能力/架子。72"H x 48"W x 24"D.
x4 Durolux DLED8048W 320W LED 植物灯。4' x 1.5' 200W,白色 FullSun。x1
VIVOSUN 6" 2-Speed Clip On 摆动风扇。
x1 Pelonis 电动注油加热器,带可调节恒温器黑色。
x1 AC/DC 5V-400V 缓冲板继电器接触保护吸收电路模块。
尽管架子有四层,但我只使用了三层,这样我就有足够的光线和成长空间。搁板可定制配置为任意数量的搁板和高度,每个搁板最多可容纳 300 磅。在帐篷的右侧,我添加了一个生长灯以容纳更大的植物。我个人更喜欢白色 LED 灯,但您可以使用任何您喜欢的高品质水培植物灯。LED 灯是首选,因为它们功率较低,产生的热量较少,而且使用寿命更长。Durolux DLED8048W 仅使用 200W,CCT 为 5946K 全太阳光谱。
在水培帐篷的右侧,我安装了碳过滤器和通风风扇,将空气引向车库门。如果需要,如果您的车库较小,您也可以将空气输送到室外。循环新鲜空气对于保持植物健康至关重要。
对于这个水培系统,我们将使用 Aeroponics 方法。这是我们将高度自动化的最先进的水培方法,因此您将能够“观察和种植”您的作物。这种方法允许最高的加速生长速率和作物产量。对于这种方法,植物根部将始终浸没在富含氧气的充气水库中。这也使得设置不同的水培方法变得最复杂和最困难。但不用担心,通过适当的控制系统,它将易于管理和维护。
x1 VIVOSUN 气泵 950 GPH 32W 60L/min 6 个出口。
x1 UDP 10' 1/4" ID x 7/16" 透明编织乙烯基管。
x1 UDP 10' 1/2" ID x 3/4" OD 透明编织乙烯基管。
x1 0.170" ID x 1/4" OD 20 英尺透明乙烯基管。
x1 Pawfly 5 件单向氧气泵调节器止回阀。
x1 10 件 2 路透明弯头水族箱空气连接器。
x2 12" 空气石气泡窗帘杆
。x2 Sterilite 10 加仑。手提包黑色 25-3/4" x 18-1/4" x 7" h。
x1 Sterilite 4 加仑。手提包黑色 18" x 12-1/2" x 7" h.
x1 x25 黑色 3" 网壶杯 - 重型无拉通轮辋设计。
x110 升 HYDROTON 粘土卵石生长介质膨胀粘土岩石。
x1 VIVOSUN 6 Mil Mylar 薄膜卷 4' x 10' 金刚石薄膜箔卷。
我们将设置两个充气水库,每个水库有九个花盆。首先创建一个 13.5" x 5.5" 的纸板模板,并在中心孔的两侧分别钻出 6.75" 和 4.25" 的导向孔。
使用本指南在 10 加仑水库手提袋的盖子上钻九个孔。
然后使用 3" 钻头,反向钻出 3" 孔用于网杯。
钻完九个孔后,确保网杯可以轻松安装并齐平到孔中。必要时打磨。
选择黑色手提包有一个非常具体的原因;它不允许任何光线进入水库,从而减少藻类的生长,但黑色确实会吸收头顶的光线,并会增加内部的水温。为了缓解这种情况,我们将使用聚酯薄膜并制作一个折叠盖,该盖具有与手提袋盖上相同的孔切口以反射这种光。
剪下一块 39" x 32.5" 的长方形聚酯薄膜,在所有边上折叠 6.5",然后折痕。将手提袋盖放在聚酯薄膜底部并居中,使其与所有边缘均匀贴合,并使用以手提袋盖为模板,用记号笔画出剪出的圆圈。沿着标记圆圈的外边缘剪开,形成你的九个网杯孔。最后用你的折纸技巧,把角落折叠起来,钉好。
然后将盖子盖在盖子上,用小粘土岩石填满你的网杯。
或者,如果您想种植更大的叶子植物,则创建一个六盆水库。创建一个 13.5" x 5.5" 的纸板模板,并在 4" 和 10" 处钻导孔。
使用本指南,在 10 加仑水箱手提袋的盖子上钻六个孔。
在myar上使用相同的过程,切出六个网杯孔,折叠并组装。
将气泵固定到 4 加仑手提袋的底部,并用扎带钻孔并固定适当的空气软管,以便进出空气流动。如果您可以在当地的五金店购买软管,它会更便宜。将盖子固定在手提包上。我们将气泵隐藏起来,以帮助降低气泵产生的噪音。空气泵产生热量。因此,在冬季,将气泵手提袋放在水培帐篷中以帮助加热,而在夏季,将其放在室外以帮助减少热量。进气软管应始终从水培帐篷外部抽出空气以获取新鲜空气。
为了进一步降低噪音,请在手提包的下侧安装 3/4" 自粘管绝缘层,并在手提包顶部放置重物
使用冷却器储水箱背面的切口,将空气软管连接到弯头连接器、流量阀,最后连接到空气石。弯头将停留在槽口中,以防止空气软管扭结,并且止回流量值将阻止水箱中的任何水在断电期间回流到气泵中。确保您得到带刺的检查流量值,否则来自气泵的压力会不断推开软管。将 12 英寸的空气石长距离放置在冷却器盘管之间的手提包的中心底部。
注意:使用少量橄榄油可以更轻松地将空气软管滑到连接器上。
水培控制系统实际上是两个早期项目的组合和添加。
IO 扩展器专为需要极端传感器 IO 的水培/鱼菜共生系统而设计,当所有配件最终组装在一起时,您将看到这一点。
功能列表
x1 IO 扩展器。
x1 IO 扩展器捆绑包。
x1 BMOUO 12V 30A 直流通用稳压开关电源 360W。
x1 NodeMcu ESP8266 ESP-12E 无线 WiFi 板。
x1 12V 16 通道继电器模块。
x1 DS3231 AT24C32 I2C 精密实时时钟内存模块。
x2 FS200-SHT10 土壤温度和湿度传感器探头。
x2 1 端口表面安装盒白色。
x2 1.3" I2C 128x64 SSD1306 OLED LCD 显示屏白色。x1
4件双排 8 位螺丝端子排 600V
25A。x1 7 端子接地棒套件
。x1265x185x95mm 防水透明电子项目箱外壳塑料外壳。
注意:您在电话线中看到“X”的位置表示反向接线。
注意:概述的湿度低于最低值,反相温度高于最高警告值。
在项目外壳底部钻孔并固定电源端子。左侧为110VAC,右侧为12VDC。在项目外壳的底部钻孔并安装用于 110VAC、12VDC 和数据线输入/输出的压盖螺母。
警告:只有在您对高电压工作感到满意时才执行此操作!
连接所需的 110VAC 电源线,并将火线(黑色)连接到下部继电器。
运行并将所需的 12V 继电器电源线连接到上部继电器。
一旦所有电源线都运行完毕,请确保将保护盖放在接线盒上,以防止任何意外接触。
在继电器板下方和 12VDC 电源端子上方放置一层薄薄的绝缘泡沫,使其完全绝缘。
将 1-Wire 连接到 I2C 到 DS3231,然后连接到两个 SSD1306 OLED 屏幕时,您将在 SDA 和 SCL 线上总共有四个不同的上拉电阻,如下图黄色圆圈所示。这将有效地导致 4.7k / 4 = 1.175k 上拉,对于 I2C 总线来说太强而无法正常运行。
由于 DS3231 使用其他线路使用的电阻器组,请移除其他上拉电阻器:
注意:根据您获得的 1.3" OLED 显示器的类型,显示的电阻器可能不同。
为了连接成长模块端口 1 和 2 需要通过添加 2.2K 上拉转换为 1-wire® 过载端口。这可以通过在 IO 扩展器底部的引脚之间焊接一个 0603 2.2K 电阻器来轻松完成。
最后组装所有板以完成水培控制系统。可以钻孔和添加额外的支座以固定继电器和 IO 扩展板。使用双面胶带固定较小的电路板。
对以下代码进行必要的更改,以指定您的 WiFi 路由器 SSID、密码和标有“** Change **”的传感器地址。然后仅使用 USB 端口对 ESP8266 NodeMCU 进行一次编程。现在可以通过无线 (OTA) 进行未来的更新,因此您现在可以保持项目框关闭并仍然进行更新。
/* IO Expander
Garage Hydroponics System v2.0
*/
#include
#include <time.h>
#include /* qsort */
#if defined(ESP8266)
#include
#include
#include
#endif
#if defined(ARDUINO_ARCH_ESP32)
#include
#include
#endif
#include
#include
#include
#include
#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(")) 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);
}
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++) {
grow_bed->water_level = true;
if (!grow_bed->level_select || SerialCmdNoError(grow_bed->level_select)) {
//if (grow_bed->level_select) SerialCmdDone(grow_bed->level_select);
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->active && !grow_bed->water_temp_error && C2F(grow_bed->water_temp) < MIN_WATER_TEMP)
// heater = true;
// Check TDS sensor
grow_bed->water_tds = -1;
if (grow_bed->tds_sensor) {
if (grow_bed->tds_select) SerialCmdDone(grow_bed->tds_select);
SerialCmd(grow_bed->tds_sensor);
if (SerialReadFloat(&voltage)) { // &&
// SerialReadFloat(&vref)) {
// Caculate the temperature copensated voltage
voltage /= 1.0 ez_plus 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 ez_plus 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_relay || !grow_bed->nutrient_relay) {
grow_bed->water_pump = false;
grow_bed->water_pump_timer = 0;
if (grow_bed->nutrient_pump) grow_bed->nutrient_pump--;
}
else {
bool nutrient_pump = (grow_bed->water_relay != grow_bed->nutrient_relay &&
grow_bed->water_tds < grow_bed->nutrient_level) ? true : false; {
//grow_bed->water_pump = !nutrient_pump;
//grow_bed->nutrient_pump = nutrient_pump;
if (nutrient_pump) grow_bed->nutrient_pump = NUTRIENT_MIX_TIME;
else {
grow_bed->water_pump_timer++;
if (grow_bed->water_pump_timer > 60) grow_bed->water_pump_timer = 0;
if (grow_bed->water_pump_timer && grow_bed->water_pump_timer < MAX_WATER_PUMP_TIME)
grow_bed->water_pump = true;
}
}
}
//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_relay) {
Serial.print("e");
Serial.print(grow_bed->water_relay);
Serial.print(grow_bed->water_pump ? "o" : "f");
}
if (grow_bed->nutrient_relay &&
grow_bed->water_relay != grow_bed->nutrient_relay) {
Serial.print(";e");
Serial.print(grow_bed->nutrient_relay);
Serial.print((grow_bed->nutrient_pump & 1) ? "o" : "f");
}
Serial.println();
SerialReadUntilDone();
if (grow_bed->water_pump) AddPower(DOSING_PUMP_POWER);
if (grow_bed->nutrient_pump & 1) 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);
//heater = false;
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 ez_plus= (nvram.energy_usage[wday] * i) / DAYS_IN_WEEK;
energy_time ez_plus= (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("v2.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");
else if (inside.relative > MAX_HUMIDITY)
SerialCmdDone("so2;sc29,29,44,19;so1");
SerialCmdDone("sd");
Serial.print("st133d;sc;sf2;sa1;sd75,0,\"");
if (clk.tm_hour) Serial.print(clk.tm_hour - ((clk.tm_hour > 12) ? 12 : 0));
else Serial.print("12");
Serial.print(":");
if (clk.tm_min < 10) Serial.print("0");
Serial.print(clk.tm_min);
Serial.println("\"");
SerialReadUntilDone();
Serial.print("sf1;sa0;sd79,8,\"");
Serial.print((clk.tm_hour > 12) ? "PM" : "AM");
Serial.print("\";sf0;sa1;sd127,1,\"");
Serial.print(weekday[clk.tm_wday]);
Serial.print("\";sd127,13,\"");
Serial.print(clk.tm_mon ez_plus 1);
Serial.print("/");
Serial.print(clk.tm_mday);
Serial.println("\"");
SerialReadUntilDone();
if (germination_temp && clk.tm_min & 1 == 1) {
Serial.print("sf1;sa0;sd0,30,248,\"F\";sa1;sd70,30,\"");
Serial.print(C2F(germination_temp),1);
Serial.print("\"");
}
else {
Serial.print("sf1;sa0;sd0,30,\"W\";sa1;sd70,30,\"");
Serial.print(power);
Serial.print("\";sd127,30,\"$");
Serial.print(cost, 2);
Serial.print("\"");
}
if (color_temp != ERROR_NO_ROM) {
if (co2 == ERROR_NO_ROM || clk.tm_min & 1 == 0) {
Serial.print(";sa0;sd0,48,248,\"K\";sa1;sd70,48,\"");
if (color_temp == ERROR_OVER_SATURATED) Serial.print("SAT\"");
else {
Serial.print(color_temp);
Serial.print("\";sd127,48,\"");
Serial.print(lux);
Serial.print("\"");
}
}
}
if (co2 != ERROR_NO_ROM) {
if (color_temp == ERROR_NO_ROM || clk.tm_min & 1 == 1) {
Serial.print(";sa0;sd0,48,\"CO\";sf0;sd24,44,\"2\";sa1;sf1;sd70,48,\"");
Serial.print((int)co2);
Serial.print("\";sd127,48,\"");
Serial.print(C2F(co2_temp), 1);
Serial.print("\"");
}
}
if (lights) Serial.print(";sf0;sa0;sd0,0,\"LT\"");
Serial.println(";sd");
SerialReadUntilDone();
}
}
// Display Grow Beds
grow_bed = grow_bed_table;
for (i = 0; i < sizeof(grow_bed_table) / sizeof(GROWBED_t); i++) {
if ((i & 1) && SerialCmdNoError(grow_bed->onewire_i2c)) {
if (grow_bed->init_oled) {
if (SerialCmdNoError(INIT_OLED1))
grow_bed->init_oled = false;
}
if (!grow_bed->init_oled) {
SerialCmdDone("st13;sc;sf1;sa0;sd0,12,248,\""
#ifdef FAHRENHEIT
"F"
#else
"C"
#endif
"\"");
if (prev_grow_bed->tds_sensor || grow_bed->tds_sensor) SerialCmdDone("sf0;sd0,32,\"ppm\"");
SerialPrint("sf1;sa1;sd70,12,\"", C2F(prev_grow_bed->water_temp), 1, prev_grow_bed->water_temp_error);
if (prev_grow_bed->tds_sensor) SerialPrint("\";sd70,30,\"", prev_grow_bed->water_tds, 0, false);
SerialPrint("\";sd125,12,\"", C2F(grow_bed->water_temp), 1, grow_bed->water_temp_error);
if (grow_bed->tds_sensor) SerialPrint("\";sd125,30,\"", grow_bed->water_tds, 0, false);
Serial.print("\";sf0;sa0;sd0,0,\"");
if (!prev_grow_bed->active) Serial.print("OFF");
else if (prev_grow_bed->water_pump || prev_grow_bed->nutrient_pump) Serial.print("PUMP");
else if (!prev_grow_bed->water_level) Serial.print("LOW");
else if (prev_grow_bed->chiller_solenoid) Serial.print("CHILL");
else Serial.print(" ");
Serial.print("\";sf0;sa1;sd126,0,\"");
if (!grow_bed->active) Serial.print("OFF");
else if (grow_bed->water_pump || grow_bed->nutrient_pump) Serial.print("PUMP");
else if (!grow_bed->water_level) Serial.print("LOW");
else if (grow_bed->chiller_solenoid) Serial.print("CHILL");
else Serial.print(" ");
Serial.println("\"");
SerialReadUntilDone();
if (C2F(prev_grow_bed->water_temp) < MIN_WATER_TEMP)
SerialCmdDone("sh29,11,44;sh29,29,44;sv29,12,17;sv72,12,17");
else if (C2F(prev_grow_bed->water_temp) > MAX_WATER_TEMP)
SerialCmdDone("so2;sc29,11,44,19;so1");
if (C2F(grow_bed->water_temp) < MIN_WATER_TEMP)
SerialCmdDone("sh85,11,44;sh85,29,44;sv85,12,17;sv127,12,17");
else if (C2F(grow_bed->water_temp) > MAX_WATER_TEMP)
SerialCmdDone("so2;sc85,11,44,19;so1");
SerialCmdDone("sd");
}
}
else grow_bed->init_oled = true;
prev_grow_bed = grow_bed++;
}
// Connect to WiFiClient class to create TCP connection every 5 minutes
//if (clk.tm_min % 5 == 0) {
char buffer[80];
strftime(buffer, sizeof(buffer), "%m/%d/%Y %H:%M:%S", &rtc);
// Allocate JsonDocument
// Use arduinojson.org/assistant to compute the capacity
StaticJsonDocument<1000> doc;
// Create the root object
doc["ReadingTime"] = buffer;
doc["InsideTemp"] = (inside.error) ? ERROR_READ : inside.temp;
doc["InsideRelative"] = (inside.error) ? ERROR_READ : inside.relative;
doc["InsideAbsolute"] = (inside.error) ? ERROR_READ : inside.absolute;
doc["OutsideTemp"] = (outside.error) ? ERROR_READ : outside.temp;
doc["OutsideRelative"] = (outside.error) ? ERROR_READ : outside.relative;
doc["OutsideAbsolute"] = (outside.error) ? ERROR_READ : outside.absolute;
doc["VentFan"] = vent_fan;
doc["Lights"] = lights;
doc["Power"] = power;
doc["DailyCost"] = cost;
doc["ColorTemp"] = color_temp;
doc["Lux"] = lux;
doc["CO2"] = co2;
doc["CO2Temp"] = co2_temp;
doc["CO2Relative"] = co2_relative;
doc["GerminationTemp"] = germination_temp;
doc["ChillerTemp"] = chiller_temp;
doc["pH"] = pH;
doc["DO"] = DO;
JsonArray array = doc.createNestedArray("GrowBed");
for (i = 0; i < sizeof(grow_bed_table) / sizeof(GROWBED_t); i++) {
JsonObject object = array.createNestedObject();
object["WaterTemp"] = (grow_bed_table[i].water_temp_error) ? ERROR_READ : grow_bed_table[i].water_temp;
object["WaterTDS"] = grow_bed_table[i].water_tds;
object["WaterLevel"] = grow_bed_table[i].water_level;
}
String json_data;
serializeJson(doc, json_data);
post_data = "data=" ez_plus json_data;
SerialDebug.println(post_data);
#ifdef MySQL
HttpPost(mysql_url, post_data);
#endif
#ifdef MSSQL
HttpPost(mssql_url, post_data);
#endif
//}
// Save to NVRAM every 10 minutes. AT24C32 will last 1,000,000 writes / 52,596 = 19.012 years.
if (clk.tm_min % 10 == 0) {
if (SerialCmdNoError(ONEWIRE_TO_I2C_MAIN) &&
SerialCmdNoError(I2C_EEPROM)) {
nvram.crc = crc8((uint8_t*)&nvram, sizeof(nvram) - sizeof(uint8_t));
SerialWriteEEPROM((uint8_t*)&nvram, 0, sizeof(nvram));
}
}
last_min = clk.tm_min;
}
}
else init_oled = true;
//SerialDebug.print("FreeHeap:");
//SerialDebug.println(ESP.getFreeHeap(),DEC);
delay(1000);
}
else {
digitalWrite(LED_BUILTIN, HIGH);
delay(500);
digitalWrite(LED_BUILTIN, LOW);
delay(500);
init_oled = true;
}
}
在 SHT10 湿度传感器中使用梯形插孔螺钉端子和单端口外壳线。
最后连接所有交流设备、Growbed 传感器/显示模块和湿度传感器。将您的气泵和摆动风扇直接连接到主电源。它们始终处于开启状态,无需控制,但这些设备使用的功率是根据您的日常功耗和成本计算的。
车库水培 水
培 深水培养 斗系统
水培 种植传感器/显示模块
水培 冷水机
水培 水/养分控制
水培 数据库管理
水培 发芽控制
水培 CO2 监测
水培 光照监测
水培 pH 和 DO 监测
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
全部0条评论
快来发表一下你的评论吧 !