電子發(fā)燒友App

硬聲App

0
  • 聊天消息
  • 系統(tǒng)消息
  • 評(píng)論與回復(fù)
登錄后你可以
  • 下載海量資料
  • 學(xué)習(xí)在線課程
  • 觀看技術(shù)視頻
  • 寫文章/發(fā)帖/加入社區(qū)
會(huì)員中心
創(chuàng)作中心

完善資料讓更多小伙伴認(rèn)識(shí)你,還能領(lǐng)取20積分哦,立即完善>

3天內(nèi)不再提示
創(chuàng)作
電子發(fā)燒友網(wǎng)>電子資料下載>電子資料>專業(yè)的水培控制系統(tǒng)開源分享

專業(yè)的水培控制系統(tǒng)開源分享

2022-10-31 | zip | 1.31 MB | 次下載 | 2積分

資料介紹

描述

車庫水培

一年 365 天種植自己的農(nóng)產(chǎn)品,產(chǎn)量比土壤高 40%。

4 周后,它們幾乎可以收割了!

首先在車庫中找到一個(gè)漂亮的空地,讓您可以進(jìn)入水培系統(tǒng)的各個(gè)方面。

大多數(shù)車庫不加熱和冷卻,所以為了幫助保持一個(gè)更穩(wěn)定的環(huán)境,絕緣是你的朋友。車庫最明顯和強(qiáng)制性的部分是首先絕緣是車庫門。使用可以在當(dāng)?shù)匚褰鸬曩I到的絕緣材料。我選擇了 Rmax R-Matte Plus-3 3/4",4 英尺 x 8 英尺的床單,我使用精確刀切割成合適的尺寸。然后將合適的尺寸水平切成兩半,并壓縮到車庫門槽中,確保箔側(cè)面向外面有半英寸的氣隙。這個(gè)氣隙給了我相當(dāng)于 6 的總 R 系數(shù)。絕緣越好,你以后支付的加熱和冷卻費(fèi)用就越少。

車庫零件

x6 Rmax R-Matte Plus-3 3/4" x 4' x 8'. R-5 聚異氰脲酸酯硬質(zhì)泡沫絕緣板

注意:確保密封車庫中允許室外空氣進(jìn)入的所有區(qū)域。

由于您將加熱、冷卻和提供人造植物光,因此建議為您的水培系統(tǒng)添加專用斷路器。讓有執(zhí)照的電工為您添加一個(gè)新的 20 安培 GFI 斷路器。大多數(shù)斷路器都在車庫中,因此添加新電路應(yīng)該是一種成本相對(duì)較低的選擇,以實(shí)現(xiàn)更好的隔離和安全。

在車庫的空地上搭建您的水培帳篷。

pYYBAGNYp5SAZg8UAADLJZSwvfU548.jpg
?

水培帳篷配件

x1 VIVOSUN 96"x48"x80" 聚酯薄膜水培種植帳篷房
x1 VIVOSUN 8" 直列管道風(fēng)扇,帶 Speeder 空氣碳過濾器管道組合
x1 Quantum Storage 4 層線架單元,300 磅負(fù)載能力/架子。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 擺動(dòng)風(fēng)扇。
x1 Pelonis 電動(dòng)注油加熱器,帶可調(diào)節(jié)恒溫器黑色。
x1 AC/DC 5V-400V 緩沖板繼電器接觸保護(hù)吸收電路模塊。

架子/燈光設(shè)置

盡管架子有四層,但我只使用了三層,這樣我就有足夠的光線和成長空間。擱板可定制配置為任意數(shù)量的擱板和高度,每個(gè)擱板最多可容納 300 磅。在帳篷的右側(cè),我添加了一個(gè)生長燈以容納更大的植物。我個(gè)人更喜歡白色 LED 燈,但您可以使用任何您喜歡的高品質(zhì)水培植物燈。LED 燈是首選,因?yàn)樗鼈児β瘦^低,產(chǎn)生的熱量較少,而且使用壽命更長。Durolux DLED8048W 僅使用 200W,CCT 為 5946K 全太陽光譜。

poYBAGNYp5iAd7paAABCKZqDd5E757.jpg
?

碳過濾器/管道設(shè)置

在水培帳篷的右側(cè),我安裝了碳過濾器和通風(fēng)風(fēng)扇,將空氣引向車庫門。如果需要,如果您的車庫較小,您也可以將空氣輸送到室外。循環(huán)新鮮空氣對(duì)于保持植物健康至關(guān)重要。

pYYBAGNYp5qAft-xAADH70eCfq0377.jpg
?

水培法

對(duì)于這個(gè)水培系統(tǒng),我們將使用 Aeroponics 方法。這是我們將高度自動(dòng)化的最先進(jìn)的水培方法,因此您將能夠“觀察和種植”您的作物。這種方法允許最高的加速生長速率和作物產(chǎn)量。對(duì)于這種方法,植物根部將始終浸沒在富含氧氣的充氣水庫中。這也使得設(shè)置不同的水培方法變得最復(fù)雜和最困難。但不用擔(dān)心,通過適當(dāng)?shù)?a target='_blank' class='arckwlink_none'>控制系統(tǒng),它將易于管理和維護(hù)。

氣培零件

x1 VIVOSUN 氣泵 950 GPH 32W 60L/min 6 個(gè)出口。
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 件單向氧氣泵調(diào)節(jié)器止回閥。
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" 網(wǎng)壺杯 - 重型無拉通輪輞設(shè)計(jì)。
x110 升 HYDROTON 粘土卵石生長介質(zhì)膨脹粘土巖石。
x1 VIVOSUN 6 Mil Mylar 薄膜卷 4' x 10' 金剛石薄膜箔卷。

氣培法設(shè)置

我們將設(shè)置兩個(gè)充氣水庫,每個(gè)水庫有九個(gè)花盆。首先創(chuàng)建一個(gè) 13.5" x 5.5" 的紙板模板,并在中心孔的兩側(cè)分別鉆出 6.75" 和 4.25" 的導(dǎo)向孔。

poYBAGNYp5yAHQmrAABXEUGGPMo052.jpg
?

使用本指南在 10 加侖水庫手提袋的蓋子上鉆九個(gè)孔。

pYYBAGNYp6GAWyy9AABeXnArD4o281.jpg
?

然后使用 3" 鉆頭,反向鉆出 3" 孔用于網(wǎng)杯。

pYYBAGNYp6WAMG05AABugB9VxEI577.jpg
?

鉆完九個(gè)孔后,確保網(wǎng)杯可以輕松安裝并齊平到孔中。必要時(shí)打磨。

poYBAGNYp6eAHdreAABhJh3bQzE349.jpg
?

選擇黑色手提包有一個(gè)非常具體的原因;它不允許任何光線進(jìn)入水庫,從而減少藻類的生長,但黑色確實(shí)會(huì)吸收頭頂?shù)墓饩€,并會(huì)增加內(nèi)部的水溫。為了緩解這種情況,我們將使用聚酯薄膜并制作一個(gè)折疊蓋,該蓋具有與手提袋蓋上相同的孔切口以反射這種光。

剪下一塊 39" x 32.5" 的長方形聚酯薄膜,在所有邊上折疊 6.5",然后折痕。將手提袋蓋放在聚酯薄膜底部并居中,使其與所有邊緣均勻貼合,并使用以手提袋蓋為模板,用記號(hào)筆畫出剪出的圓圈。沿著標(biāo)記圓圈的外邊緣剪開,形成你的九個(gè)網(wǎng)杯孔。最后用你的折紙技巧,把角落折疊起來,釘好。

poYBAGNYp6mAD1HoAABYNWu_oAg106.jpg
?

然后將蓋子蓋在蓋子上,用小粘土巖石填滿你的網(wǎng)杯。

poYBAGNYp62ADWF8AADaulPSj0Y220.jpg
?

或者,如果您想種植更大的葉子植物,則創(chuàng)建一個(gè)六盆水庫。創(chuàng)建一個(gè) 13.5" x 5.5" 的紙板模板,并在 4" 和 10" 處鉆導(dǎo)孔。

pYYBAGNYp7CABEjFAABY3vVpm1k584.jpg
?

使用本指南,在 10 加侖水箱手提袋的蓋子上鉆六個(gè)孔。

poYBAGNYp7OASYjsAABvWTi1Pxs975.jpg
?

在myar上使用相同的過程,切出六個(gè)網(wǎng)杯孔,折疊并組裝。

poYBAGNYp7WAHYXAAABr1bwX_i0042.jpg
?

將氣泵固定到 4 加侖手提袋的底部,并用扎帶鉆孔并固定適當(dāng)?shù)目諝廛浌埽员氵M(jìn)出空氣流動(dòng)。如果您可以在當(dāng)?shù)氐奈褰鸬曩徺I軟管,它會(huì)更便宜。將蓋子固定在手提包上。我們將氣泵隱藏起來,以幫助降低氣泵產(chǎn)生的噪音。空氣泵產(chǎn)生熱量。因此,在冬季,將氣泵手提袋放在水培帳篷中以幫助加熱,而在夏季,將其放在室外以幫助減少熱量。進(jìn)氣軟管應(yīng)始終從水培帳篷外部抽出空氣以獲取新鮮空氣。

poYBAGNYp7eAQH67AACCuMyJvLU554.jpg
?

為了進(jìn)一步降低噪音,請(qǐng)?jiān)谑痔岚南聜?cè)安裝 3/4" 自粘管絕緣層,并在手提包頂部放置重物

pYYBAGNYp7qAbFxIAADDTF8R01A381.jpg
?

使用冷卻器儲(chǔ)水箱背面的切口,將空氣軟管連接到彎頭連接器、流量閥,最后連接到空氣石。彎頭將停留在槽口中,以防止空氣軟管扭結(jié),并且止回流量值將阻止水箱中的任何水在斷電期間回流到氣泵中。確保您得到帶刺的檢查流量值,否則來自氣泵的壓力會(huì)不斷推開軟管。將 12 英寸的空氣石長距離放置在冷卻器盤管之間的手提包的中心底部。

注意:使用少量橄欖油可以更輕松地將空氣軟管滑到連接器上。

poYBAGNYp72AO3rDAACPNegaeuQ626.jpg
?

水培控制系統(tǒng)

水培控制系統(tǒng)實(shí)際上是兩個(gè)早期項(xiàng)目的組合和添加。

IO 擴(kuò)展器專為需要極端傳感器 IO 的水培/魚菜共生系統(tǒng)設(shè)計(jì),當(dāng)所有配件最終組裝在一起時(shí),您將看到這一點(diǎn)。

pYYBAGNYp7-AbD5hAADziVWgUcg223.jpg
?

功能列表

  • 內(nèi)部/外部溫度/濕度傳感器
  • 具有絕對(duì)濕度比較的智能通風(fēng)風(fēng)扇控制。
  • 智能通風(fēng)省電。
  • 照明控制。
  • 自動(dòng)溫度控制。
  • 用于調(diào)度的電池支持實(shí)時(shí)時(shí)鐘
  • 非易失性存儲(chǔ)。備份當(dāng)前狀態(tài)。
  • 智能電源控制和監(jiān)控。
  • WiFi 連接。
  • WiFi記錄實(shí)時(shí)數(shù)據(jù)。
  • WiFi 提醒您的智能手機(jī)。

水培控制系統(tǒng)零件

x1 IO 擴(kuò)展器
x1 IO 擴(kuò)展器捆綁包。
x1 BMOUO 12V 30A 直流通用穩(wěn)壓開關(guān)電源 360W。
x1 NodeMcu ESP8266 ESP-12E 無線 WiFi 板。
x1 12V 16 通道繼電器模塊。
x1 DS3231 AT24C32 I2C 精密實(shí)時(shí)時(shí)鐘內(nèi)存模塊。
x2 FS200-SHT10 土壤溫度和濕度傳感器探頭。
x2 1 端口表面安裝盒白色。
x2 1.3" I2C 128x64 SSD1306 OLED LCD 顯示屏白色。x1
4件雙排 8 位螺絲端子排 600V
25A。x1 7 端子接地棒套件
。x1265x185x95mm 防水透明電子項(xiàng)目箱外殼塑料外殼。

接線圖

poYBAGNYp8SAPnNmAAWdhKaBITE157.jpg
?

注意:您在電話線中看到“X”的位置表示反向接線。

OLED顯示屏

pYYBAGNYp8eAd8FnAACOO7nnjxk295.jpg
?

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

那么為什么要使用 IO 擴(kuò)展器呢?

  • 設(shè)計(jì)更簡單。
  • 現(xiàn)成的零件。
  • 無需寫入 1-Wire 驅(qū)動(dòng)程序。
  • 沒有要寫入的繼電器驅(qū)動(dòng)程序。
  • 無需編寫 OLED 顯示驅(qū)動(dòng)程序。
  • 沒有顯示字體占用 ESP8266 代碼空間。
  • 無需編寫濕度傳感器驅(qū)動(dòng)程序。
  • 無需寫入 DS3231 RTC 驅(qū)動(dòng)程序。
  • 無需寫入 AT24C32 EEPROM 驅(qū)動(dòng)程序。
  • 節(jié)省 ESP8266 上的代碼空間。
  • 使用標(biāo)準(zhǔn) RJ11 電話線易于接線。
  • 沒有傳感器電纜長度問題。
  • 比商業(yè)系統(tǒng)更便宜。
  • 易于更改以適應(yīng)個(gè)人需求。
  • 單電源。

水培控制系統(tǒng)

在項(xiàng)目外殼底部鉆孔并固定電源端子。左側(cè)為110VAC,右側(cè)為12VDC。在項(xiàng)目外殼的底部鉆孔并安裝用于 110VAC、12VDC 和數(shù)據(jù)線輸入/輸出的壓蓋螺母。

pYYBAGNYp8mAEpE6AACp5ArHHEg414.jpg
?

警告:只有在您對(duì)高電壓工作感到滿意時(shí)才執(zhí)行此操作!

pYYBAGNYp8uAPcgjAAAy9VbPPJE264.jpg
?

連接所需的 110VAC 電源線,并將火線(黑色)連接到下部繼電器。

poYBAGNYp86ATWDhAADHNuTrBvg853.jpg
?

運(yùn)行并將所需的 12V 繼電器電源線連接到上部繼電器。

poYBAGNYp9OAN_wyAADbglxUatg741.jpg
?

一旦所有電源線都運(yùn)行完畢,請(qǐng)確保將保護(hù)蓋放在接線盒上,以防止任何意外接觸。

在繼電器板下方和 12VDC 電源端子上方放置一層薄薄的絕緣泡沫,使其完全絕緣。

poYBAGNYp9aAKo0-AADF8jDqivI044.jpg
?

將 1-Wire 連接到 I2C 到 DS3231,然后連接到兩個(gè) SSD1306 OLED 屏幕時(shí),您將在 SDA 和 SCL 線上總共有四個(gè)不同的上拉電阻,如下圖黃色圓圈所示。這將有效地導(dǎo)致 4.7k / 4 = 1.175k 上拉,對(duì)于 I2C 總線來說太強(qiáng)而無法正常運(yùn)行。

pYYBAGNYp9iAZYYqAADc0C3VKxk265.jpg
?

由于 DS3231 使用其他線路使用的電阻器組,請(qǐng)移除其他上拉電阻器:

  • 1-Wire 到 I2C R3 和 R4。
  • SSD1306 OLED R6 和 R7。
  • 將第二個(gè) OLED 屏幕上以綠色圈出的 4.7k 上拉從地址選擇 0x78 移動(dòng)到 0x7A。

注意:根據(jù)您獲得的 1.3" OLED 顯示器的類型,顯示的電阻器可能不同。

為了連接成長模塊端口 1 和 2 需要通過添加 2.2K 上拉轉(zhuǎn)換為 1-wire? 過載端口。這可以通過在 IO 擴(kuò)展器底部的引腳之間焊接一個(gè) 0603 2.2K 電阻器來輕松完成。

poYBAGNYp9qAGqgnAABwWr65SB0272.jpg
?

最后組裝所有板以完成水培控制系統(tǒng)。可以鉆孔和添加額外的支座以固定繼電器和 IO 擴(kuò)展板。使用雙面膠帶固定較小的電路板。

poYBAGNYp92ANTS5AAD4OwKqd4c070.jpg
?

ESP8266 代碼 (OTA)

對(duì)以下代碼進(jìn)行必要的更改,以指定您的 WiFi 路由器 SSID、密碼和標(biāo)有“** Change **”的傳感器地址。然后僅使用 USB 端口對(duì) ESP8266 NodeMCU 進(jìn)行一次編程。現(xiàn)在可以通過無線 (OTA) 進(jìn)行未來的更新,因此您現(xiàn)在可以保持項(xiàng)目框關(guān)閉并仍然進(jìn)行更新。

/* 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
?

設(shè)置圖

最后連接所有交流設(shè)備、Growbed 傳感器/顯示模塊和濕度傳感器。將您的氣泵和擺動(dòng)風(fēng)扇直接連接到主電源。它們始終處于開啟狀態(tài),無需控制,但這些設(shè)備使用的功率是根據(jù)您的日常功耗和成本計(jì)算的。

?

poYBAGNYp-SAfyHPAAOBmRBbmU0042.jpg
?

有關(guān)完整的車庫水培解決方案,請(qǐng)參閱我們的其他項(xiàng)目

車庫水培 水
培 深水培養(yǎng) 斗系統(tǒng)
水培 種植傳感器/顯示模塊
水培 冷水機(jī)
水培 水/養(yǎng)分控制
水培 數(shù)據(jù)庫管理
水培 發(fā)芽控制
水培 CO2 監(jiān)測
水培 光照監(jiān)測
水培 pH 和 DO 監(jiān)測


下載該資料的人也在下載 下載該資料的人還在閱讀
更多 >

評(píng)論

查看更多

下載排行

本周

  1. 1山景DSP芯片AP8248A2數(shù)據(jù)手冊(cè)
  2. 1.06 MB  |  532次下載  |  免費(fèi)
  3. 2RK3399完整板原理圖(支持平板,盒子VR)
  4. 3.28 MB  |  339次下載  |  免費(fèi)
  5. 3TC358743XBG評(píng)估板參考手冊(cè)
  6. 1.36 MB  |  330次下載  |  免費(fèi)
  7. 4DFM軟件使用教程
  8. 0.84 MB  |  295次下載  |  免費(fèi)
  9. 5元宇宙深度解析—未來的未來-風(fēng)口還是泡沫
  10. 6.40 MB  |  227次下載  |  免費(fèi)
  11. 6迪文DGUS開發(fā)指南
  12. 31.67 MB  |  194次下載  |  免費(fèi)
  13. 7元宇宙底層硬件系列報(bào)告
  14. 13.42 MB  |  182次下載  |  免費(fèi)
  15. 8FP5207XR-G1中文應(yīng)用手冊(cè)
  16. 1.09 MB  |  178次下載  |  免費(fèi)

本月

  1. 1OrCAD10.5下載OrCAD10.5中文版軟件
  2. 0.00 MB  |  234315次下載  |  免費(fèi)
  3. 2555集成電路應(yīng)用800例(新編版)
  4. 0.00 MB  |  33566次下載  |  免費(fèi)
  5. 3接口電路圖大全
  6. 未知  |  30323次下載  |  免費(fèi)
  7. 4開關(guān)電源設(shè)計(jì)實(shí)例指南
  8. 未知  |  21549次下載  |  免費(fèi)
  9. 5電氣工程師手冊(cè)免費(fèi)下載(新編第二版pdf電子書)
  10. 0.00 MB  |  15349次下載  |  免費(fèi)
  11. 6數(shù)字電路基礎(chǔ)pdf(下載)
  12. 未知  |  13750次下載  |  免費(fèi)
  13. 7電子制作實(shí)例集錦 下載
  14. 未知  |  8113次下載  |  免費(fèi)
  15. 8《LED驅(qū)動(dòng)電路設(shè)計(jì)》 溫德爾著
  16. 0.00 MB  |  6656次下載  |  免費(fèi)

總榜

  1. 1matlab軟件下載入口
  2. 未知  |  935054次下載  |  免費(fèi)
  3. 2protel99se軟件下載(可英文版轉(zhuǎn)中文版)
  4. 78.1 MB  |  537798次下載  |  免費(fèi)
  5. 3MATLAB 7.1 下載 (含軟件介紹)
  6. 未知  |  420027次下載  |  免費(fèi)
  7. 4OrCAD10.5下載OrCAD10.5中文版軟件
  8. 0.00 MB  |  234315次下載  |  免費(fèi)
  9. 5Altium DXP2002下載入口
  10. 未知  |  233046次下載  |  免費(fèi)
  11. 6電路仿真軟件multisim 10.0免費(fèi)下載
  12. 340992  |  191187次下載  |  免費(fèi)
  13. 7十天學(xué)會(huì)AVR單片機(jī)與C語言視頻教程 下載
  14. 158M  |  183279次下載  |  免費(fèi)
  15. 8proe5.0野火版下載(中文版免費(fèi)下載)
  16. 未知  |  138040次下載  |  免費(fèi)