×

专业的水培控制系统开源分享

消耗积分:2 | 格式:zip | 大小:1.31 MB | 2022-10-27

鼠爱米

分享资料个

描述

车库水培

一年 365 天种植自己的农产品,产量比土壤高 40%。

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 断路器。大多数断路器都在车库中,因此添加新电路应该是一种成本相对较低的选择,以实现更好的隔离和安全。

在车库的空地上搭建您的水培帐篷。

pYYBAGNYp5SAZg8UAADLJZSwvfU548.jpg
 

水培帐篷配件

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 全太阳光谱。

poYBAGNYp5iAd7paAABCKZqDd5E757.jpg
 

碳过滤器/管道设置

在水培帐篷的右侧,我安装了碳过滤器和通风风扇,将空气引向车库门。如果需要,如果您的车库较小,您也可以将空气输送到室外。循环新鲜空气对于保持植物健康至关重要。

pYYBAGNYp5qAft-xAADH70eCfq0377.jpg
 

水培法

对于这个水培系统,我们将使用 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" 的导向孔。

poYBAGNYp5yAHQmrAABXEUGGPMo052.jpg
 

使用本指南在 10 加仑水库手提袋的盖子上钻九个孔。

pYYBAGNYp6GAWyy9AABeXnArD4o281.jpg
 

然后使用 3" 钻头,反向钻出 3" 孔用于网杯。

pYYBAGNYp6WAMG05AABugB9VxEI577.jpg
 

钻完九个孔后,确保网杯可以轻松安装并齐平到孔中。必要时打磨。

poYBAGNYp6eAHdreAABhJh3bQzE349.jpg
 

选择黑色手提包有一个非常具体的原因;它不允许任何光线进入水库,从而减少藻类的生长,但黑色确实会吸收头顶的光线,并会增加内部的水温。为了缓解这种情况,我们将使用聚酯薄膜并制作一个折叠盖,该盖具有与手提袋盖上相同的孔切口以反射这种光。

剪下一块 39" x 32.5" 的长方形聚酯薄膜,在所有边上折叠 6.5",然后折痕。将手提袋盖放在聚酯薄膜底部并居中,使其与所有边缘均匀贴合,并使用以手提袋盖为模板,用记号笔画出剪出的圆圈。沿着标记圆圈的外边缘剪开,形成你的九个网杯孔。最后用你的折纸技巧,把角落折叠起来,钉好。

poYBAGNYp6mAD1HoAABYNWu_oAg106.jpg
 

然后将盖子盖在盖子上,用小粘土岩石填满你的网杯。

poYBAGNYp62ADWF8AADaulPSj0Y220.jpg
 

或者,如果您想种植更大的叶子植物,则创建一个六盆水库。创建一个 13.5" x 5.5" 的纸板模板,并在 4" 和 10" 处钻导孔。

pYYBAGNYp7CABEjFAABY3vVpm1k584.jpg
 

使用本指南,在 10 加仑水箱手提袋的盖子上钻六个孔。

poYBAGNYp7OASYjsAABvWTi1Pxs975.jpg
 

在myar上使用相同的过程,切出六个网杯孔,折叠并组装。

poYBAGNYp7WAHYXAAABr1bwX_i0042.jpg
 

将气泵固定到 4 加仑手提袋的底部,并用扎带钻孔并固定适当的空气软管,以便进出空气流动。如果您可以在当地的五金店购买软管,它会更便宜。将盖子固定在手提包上。我们将气泵隐藏起来,以帮助降低气泵产生的噪音。空气泵产生热量。因此,在冬季,将气泵手提袋放在水培帐篷中以帮助加热,而在夏季,将其放在室外以帮助减少热量。进气软管应始终从水培帐篷外部抽出空气以获取新鲜空气。

poYBAGNYp7eAQH67AACCuMyJvLU554.jpg
 

为了进一步降低噪音,请在手提包的下侧安装 3/4" 自粘管绝缘层,并在手提包顶部放置重物

pYYBAGNYp7qAbFxIAADDTF8R01A381.jpg
 

使用冷却器储水箱背面的切口,将空气软管连接到弯头连接器、流量阀,最后连接到空气石。弯头将停留在槽口中,以防止空气软管扭结,并且止回流量值将阻止水箱中的任何水在断电期间回流到气泵中。确保您得到带刺的检查流量值,否则来自气泵的压力会不断推开软管。将 12 英寸的空气石长距离放置在冷却器盘管之间的手提包的中心底部。

注意:使用少量橄榄油可以更轻松地将空气软管滑到连接器上。

poYBAGNYp72AO3rDAACPNegaeuQ626.jpg
 

水培控制系统

水培控制系统实际上是两个早期项目的组合和添加。

IO 扩展器专为需要极端传感器 IO 的水培/鱼菜共生系统设计,当所有配件最终组装在一起时,您将看到这一点。

pYYBAGNYp7-AbD5hAADziVWgUcg223.jpg
 

功能列表

  • 内部/外部温度/湿度传感器。
  • 具有绝对湿度比较的智能通风风扇控制。
  • 智能通风省电。
  • 照明控制。
  • 自动温度控制。
  • 用于调度的电池支持实时时钟。
  • 非易失性存储。备份当前状态。
  • 智能电源控制和监控。
  • WiFi 连接。
  • WiFi记录实时数据。
  • WiFi 提醒您的智能手机。

水培控制系统零件

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 防水透明电子项目箱外壳塑料外壳。

接线图

poYBAGNYp8SAPnNmAAWdhKaBITE157.jpg
 

注意:您在电话线中看到“X”的位置表示反向接线。

OLED显示屏

pYYBAGNYp8eAd8FnAACOO7nnjxk295.jpg
 

注意:概述的湿度低于最低值,反相温度高于最高警告值。

那么为什么要使用 IO 扩展器呢?

  • 设计更简单。
  • 现成的零件。
  • 无需写入 1-Wire 驱动程序。
  • 没有要写入的继电器驱动程序。
  • 无需编写 OLED 显示驱动程序。
  • 没有显示字体占用 ESP8266 代码空间。
  • 无需编写湿度传感器驱动程序。
  • 无需写入 DS3231 RTC 驱动程序。
  • 无需写入 AT24C32 EEPROM 驱动程序。
  • 节省 ESP8266 上的代码空间。
  • 使用标准 RJ11 电话线易于接线。
  • 没有传感器电缆长度问题。
  • 比商业系统更便宜。
  • 易于更改以适应个人需求。
  • 单电源。

水培控制系统

在项目外壳底部钻孔并固定电源端子。左侧为110VAC,右侧为12VDC。在项目外壳的底部钻孔并安装用于 110VAC、12VDC 和数据线输入/输出的压盖螺母。

pYYBAGNYp8mAEpE6AACp5ArHHEg414.jpg
 

警告:只有在您对高电压工作感到满意时才执行此操作!

pYYBAGNYp8uAPcgjAAAy9VbPPJE264.jpg
 

连接所需的 110VAC 电源线,并将火线(黑色)连接到下部继电器。

poYBAGNYp86ATWDhAADHNuTrBvg853.jpg
 

运行并将所需的 12V 继电器电源线连接到上部继电器。

poYBAGNYp9OAN_wyAADbglxUatg741.jpg
 

一旦所有电源线都运行完毕,请确保将保护盖放在接线盒上,以防止任何意外接触。

在继电器板下方和 12VDC 电源端子上方放置一层薄薄的绝缘泡沫,使其完全绝缘。

poYBAGNYp9aAKo0-AADF8jDqivI044.jpg
 

将 1-Wire 连接到 I2C 到 DS3231,然后连接到两个 SSD1306 OLED 屏幕时,您将在 SDA 和 SCL 线上总共有四个不同的上拉电阻,如下图黄色圆圈所示。这将有效地导致 4.7k / 4 = 1.175k 上拉,对于 I2C 总线来说太强而无法正常运行。

pYYBAGNYp9iAZYYqAADc0C3VKxk265.jpg
 

由于 DS3231 使用其他线路使用的电阻器组,请移除其他上拉电阻器:

  • 1-Wire 到 I2C R3 和 R4。
  • SSD1306 OLED R6 和 R7。
  • 将第二个 OLED 屏幕上以绿色圈出的 4.7k 上拉从地址选择 0x78 移动到 0x7A。

注意:根据您获得的 1.3" OLED 显示器的类型,显示的电阻器可能不同。

为了连接成长模块端口 1 和 2 需要通过添加 2.2K 上拉转换为 1-wire® 过载端口。这可以通过在 IO 扩展器底部的引脚之间焊接一个 0603 2.2K 电阻器来轻松完成。

poYBAGNYp9qAGqgnAABwWr65SB0272.jpg
 

最后组装所有板以完成水培控制系统。可以钻孔和添加额外的支座以固定继电器和 IO 扩展板。使用双面胶带固定较小的电路板。

poYBAGNYp92ANTS5AAD4OwKqd4c070.jpg
 

ESP8266 代码 (OTA)

对以下代码进行必要的更改,以指定您的 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 湿度传感器中使用梯形插孔螺钉端子和单端口外壳线。

poYBAGNYp-GAcMGrAABl1KfOw0g317.jpg
 

设置图

最后连接所有交流设备、Growbed 传感器/显示模块和湿度传感器。将您的气泵和摆动风扇直接连接到主电源。它们始终处于开启状态,无需控制,但这些设备使用的功率是根据您的日常功耗和成本计算的。

 

poYBAGNYp-SAfyHPAAOBmRBbmU0042.jpg
 

有关完整的车库水培解决方案,请参阅我们的其他项目

车库水培 水
培 深水培养 斗系统
水培 种植传感器/显示模块
水培 冷水机
水培 水/养分控制
水培 数据库管理
水培 发芽控制
水培 CO2 监测
水培 光照监测
水培 pH 和 DO 监测


声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

评论(0)
发评论

下载排行榜

全部0条评论

快来发表一下你的评论吧 !