因篇幅關係,拆成以下幾篇易於閱讀。
- 透過Home Assistant實做魚缸的監控與輔助管理之一:系統概要
- 透過Home Assistant實做魚缸的監控與輔助管理之二:感應器與ESP32 (本文)
- 透過Home Assistant實做魚缸的監控與輔助管理之三:實現輔助管理
- 透過Home Assistant實做魚缸的監控與輔助管理之四:操作介面
這篇先講一下怎麼把所有的感應器透過ESP32接出來,我的ESP32有二塊,分別命名為ESP32_Main與ESP32_Sub,之所以獨立出ESP32_Sub出來是因為pH模組遇到我無法解決的電路干擾問題。二塊的作業如下
這是感應器腳位與GPIO的對照表(省略GND),什麼感應器要插哪一個GPIO當然是看自己開心,但要留意一下GPIO對應ADC1或ADC2,ADC2的GPIO可能會跟Wi-Fi衝突。
ESP_MAIN | VCC | Data |
主缸水溫線 | 3.3V | 13 (OneWire) |
補水桶水溫線 | 3.3V | 13 (OneWire) |
TDS模組 | 3.3V | GPIO 39 |
非接觸式液體感應器 | 3.3V | GPIO 35 |
主缸水位浮球開關 | 無 | GPIO 26 |
濾材格水位浮球開關 | 無 | GPIO 12 |
馬達格高水位浮球開關 | 無 | GPIO 14 |
馬達格低水位浮球開關 | 無 | GPIO 27 |
1602A LCD | 3.3V | 32 (SDA), 33 (SCL) |
ESP_MAIN對應的程式碼如下,需要引入Wi-Fi與MQTT的Library。
// 引入必要的Library
#include <WiFi.h> // Wi-Fi
#include <OneWire.h> // 水溫線用
#include <DallasTemperature.h> // 水溫線用
#include <PubSubClient.h> // MQTT
#include <Wire.h> // LCD用
#include <LiquidCrystal_I2C.h> // LCD用
// 定義Wi-Fi SSID與密碼
const char* ssid = "WiFiSSID";
const char* password = "WiFiPassword";
// 定義MQTT的伺服器ip、帳號與密碼
const char* mqtt_server = "192.168.xxx.yyy";
const char* mqtt_user = "mqtt_user";
const char* mqtt_password = "mqtt_password";
// 定義要發佈的MQTT topic
const char* mqtt_topic_maintank_tds = "aquarium/maintank/tds";
const char* mqtt_topic_maintank_watertemperature = "aquarium/maintank/water_temperature";
const char* mqtt_topic_maintank_water_lvl = "aquarium/maintank/water_level";
const char* mqtt_topic_bucket_watertemperature = "aquarium/bucket/water_temperature";
const char* mqtt_topic_bucket_water_lvl = "aquarium/bucket/water_level";
const char* mqtt_topic_bucket_water_lvl_voltage = "aquarium/bucket/water_level_voltage";
const char* mqtt_topic_filtertank_water_lvl = "aquarium/filtertank/water_level";
const char* mqtt_topic_pumptank_water_lvl = "aquarium/pumptank/water_level";
const char* mqtt_topic_esp32_heartbeat = "aquarium/others/esp32_heartbeat_main";
// 定義各感應器的GPIO
#define TDS_SENSOR_PIN 39
#define BUCKET_WATER_LVL_SENSOR_PIN 35
#define SDA_PIN 32
#define SCL_PIN 33
#define MAINTANK_WATER_LVL_FLOATSWITCH_PIN 26
#define PUMPTANK_WATER_LOWLVL_FLOATSWITCH_PIN 27
#define PUMPTANK_WATER_HIGHLVL_FLOATSWITCH_PIN 14
#define FILTERTANK_WATER_LVL_FLOATSWITCH_PIN 12
#define ONE_WIRE_BUS_PIN 13
// 設定參考電壓
#define VREF 3.3
// 設定TDS
#define TDS_SampleNum 30
int TDS_SampleFreq = 500;
int TDS_analogBuffer[TDS_SampleNum];
int TDS_analogBufferIndex = 0;
float TDS_Value = 0; // 初始值
// 設定pH
float pH_Value = 7; // 初始值
float pH_Voltage = 0; // 初始值
// 設定水溫
DeviceAddress maintank_temp, bucket_temp;
OneWire oneWire(ONE_WIRE_BUS_PIN);
DallasTemperature sensors(&oneWire);
float temperatureC1 = 25; // 初始值
float temperatureC2 = 25; // 初始值
// 設定水位初始值
int bucket_water_lvl = -1;
int pumptank_water_lvl = -1;
int maintank_water_lvl = -1;
int filtertank_water_lvl = -1;
// 設定HeartBeat
int loop_number = 0;
// 設定LCD
LiquidCrystal_I2C lcd(0x27, 16, 2);
// 設定Wi-Fi與MQTT的連結
WiFiClient espClient;
PubSubClient client(espClient);
// 設定Wi-Fi的函式
void setup_wifi()
{
delay(1000);
Serial.print("Connecting to WiFi ");
Serial.print(ssid);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED)
{
delay(500);
Serial.print(".");
}
Serial.println("");
Serial.println("WiFi is connected");
Serial.print("IP address: ");
Serial.println(WiFi.localIP());
}
// 設定MQTT的函式
void setup_mqtt()
{
while (!client.connected())
{
Serial.print("Connecting to MQTT");
if (client.connect("ESP32_AquaController_main", mqtt_user, mqtt_password))
{
Serial.println("");
Serial.println("MQTT is connected");
}
else
{
Serial.println("");
Serial.print("Failed, error code ");
Serial.print(client.state());
Serial.println(", try again in 5 seconds.");
delay(5000);
}
}
}
// 設定MQTT訂閱主題的函式
void callback(char* topic, byte* payload, unsigned int length)
{
payload[length] = '\0';
String message = String((char*)payload);
if (strcmp(topic, "aquarium/maintank/ph") == 0)
{
pH_Value = message.toFloat();
}
else if (strcmp(topic, "aquarium/maintank/ph_voltage") == 0)
{
pH_Voltage = message.toFloat();
}
}
// 初始化函式。這個函式只會在設備開機或重啟時執行一次。
void setup()
{
Serial.begin(115200);
sensors.begin();
sensors.getAddress(maintank_temp, 0);
sensors.getAddress(bucket_temp, 1);
Wire.begin(SDA_PIN, SCL_PIN);
lcd.init();
lcd.backlight();
pinMode(TDS_SENSOR_PIN, INPUT);
pinMode(BUCKET_WATER_LVL_SENSOR_PIN, INPUT);
pinMode(MAINTANK_WATER_LVL_FLOATSWITCH_PIN, INPUT_PULLUP);
pinMode(FILTERTANK_WATER_LVL_FLOATSWITCH_PIN, INPUT_PULLUP);
pinMode(PUMPTANK_WATER_HIGHLVL_FLOATSWITCH_PIN, INPUT_PULLUP);
pinMode(PUMPTANK_WATER_LOWLVL_FLOATSWITCH_PIN, INPUT_PULLUP);
setup_wifi();
client.setServer(mqtt_server, 1883);
client.setCallback(callback);
if (!client.connected())
{
setup_mqtt();
}
client.subscribe("aquarium/maintank/ph");
client.subscribe("aquarium/maintank/ph_voltage");
}
// 主函式,固定時間發佈TDS、水溫與水位的資訊。
void loop()
{
if (!client.connected())
{
setup_mqtt();
}
client.loop();
// TDS
static unsigned long TDS_SampleTime = 0;
if(millis() - TDS_SampleTime > TDS_SampleFreq)
{
TDS_SampleTime = millis();
TDS_analogBuffer[TDS_analogBufferIndex] = analogRead(TDS_SENSOR_PIN);
TDS_analogBufferIndex++;
if(TDS_analogBufferIndex == TDS_SampleNum)
{
TDS_analogBufferIndex = 0;
}
}
static unsigned long lastRPT_TDS = 0;
if (millis() - lastRPT_TDS > 5000)
{
lastRPT_TDS = millis();
float sum = 0;
for(int i = 0; i < TDS_SampleNum; i++)
{
sum += TDS_analogBuffer[i];
}
float TDSVoltage = (sum / TDS_SampleNum) * (float)VREF / 4095;
float compensationCoefficient = 1.0 + 0.02 * (temperatureC1 - 25.0);
float compensationVoltage = TDSVoltage / compensationCoefficient;
TDS_Value = (133.42 * compensationVoltage * compensationVoltage * compensationVoltage - 255.86 * compensationVoltage * compensationVoltage + 857.39 * compensationVoltage) * 0.5;
TDS_Value = (int)TDS_Value;
client.publish(mqtt_topic_maintank_tds, String(TDS_Value).c_str(), true);
}
// 水溫報告
static unsigned long lastRPT_Temp = 0;
if (millis() - lastRPT_Temp > 3000)
{
lastRPT_Temp = millis();
sensors.requestTemperatures();
temperatureC1 = round(sensors.getTempC(maintank_temp) * 10) / 10.0;
client.publish(mqtt_topic_maintank_watertemperature, String(temperatureC1).c_str(), true);
temperatureC2 = round(sensors.getTempC(bucket_temp) * 10) / 10.0;
client.publish(mqtt_topic_bucket_watertemperature, String(temperatureC2).c_str(), true);
}
// 補水桶水位變化時報告
static unsigned long lastRPT_BucketWaterLevel = 0;
if (millis() - lastRPT_BucketWaterLevel > 1050)
{
lastRPT_BucketWaterLevel = millis();
int bucket_water_lvl_Reading = analogRead(BUCKET_WATER_LVL_SENSOR_PIN);
if (bucket_water_lvl_Reading >= 2048)
{
bucket_water_lvl = 2;
}
else
{
bucket_water_lvl = 0;
}
client.publish(mqtt_topic_bucket_water_lvl, String(bucket_water_lvl).c_str(), true);
client.publish(mqtt_topic_bucket_water_lvl_voltage, String(bucket_water_lvl_Reading).c_str(), true);
}
// 主缸水位變化時報告
static unsigned long lastRPT_MaintankWaterLevel = 0;
if (millis() - lastRPT_MaintankWaterLevel > 1000)
{
lastRPT_MaintankWaterLevel = millis();
int maintank_water_lvl = digitalRead(MAINTANK_WATER_LVL_FLOATSWITCH_PIN);
maintank_water_lvl = (maintank_water_lvl == 1) ? 2 : 0;
client.publish(mqtt_topic_maintank_water_lvl, String(maintank_water_lvl).c_str(), true);
}
// 過濾格水位變化時報告
static unsigned long lastRPT_FiltertankWaterLevel = 0;
if (millis() - lastRPT_FiltertankWaterLevel > 1000)
{
lastRPT_FiltertankWaterLevel = millis();
int filtertank_water_lvl = digitalRead(FILTERTANK_WATER_LVL_FLOATSWITCH_PIN);
filtertank_water_lvl = (filtertank_water_lvl == 1) ? 2 : 0;
client.publish(mqtt_topic_filtertank_water_lvl, String(filtertank_water_lvl).c_str(), true);
}
// 馬達格水位變化時報告
static unsigned long lastRPT_PumptankWaterLevel = 0;
if (millis() - lastRPT_PumptankWaterLevel > 1000)
{
lastRPT_PumptankWaterLevel = millis();
int pumptank_water_highlvl = digitalRead(PUMPTANK_WATER_HIGHLVL_FLOATSWITCH_PIN);
int pumptank_water_lowlvl = digitalRead(PUMPTANK_WATER_LOWLVL_FLOATSWITCH_PIN);
if (pumptank_water_lowlvl == 0 && pumptank_water_highlvl == 0)
{
pumptank_water_lvl = 0;
}
else if (pumptank_water_lowlvl == 1 && pumptank_water_highlvl == 0)
{
pumptank_water_lvl = 1;
}
else if (pumptank_water_lowlvl == 1 && pumptank_water_highlvl == 1)
{
pumptank_water_lvl = 2;
}
else
{
pumptank_water_lvl = -999;
}
client.publish(mqtt_topic_pumptank_water_lvl, String(pumptank_water_lvl).c_str(), true);
}
// Heartbeat
static unsigned long lastRPT_HeartBeat = 0;
if (millis() - lastRPT_HeartBeat >= 60000)
{
lastRPT_HeartBeat = millis();
loop_number++;
client.publish(mqtt_topic_esp32_heartbeat, String(loop_number).c_str(), true);
}
// LCD
lcd.setCursor(0, 0);
lcd.print("TEMP:");
lcd.print(String(temperatureC1, 1));
lcd.print("/");
lcd.print(String(temperatureC2, 1));
lcd.print(" ");
lcd.setCursor(0, 1);
lcd.print("TDS:");
lcd.print((int)TDS_Value);
lcd.print(" pH:");
lcd.print((float)pH_Value);
lcd.print(" ");
}
ESP_SUB只有一個pH模組
ESP_SUB | VCC | Data |
pH | 5V | 34 |
ESP_SUB對應的程式碼如下,也需要引入Wi-Fi與MQTT的Library,其實都大同小異。
#include <WiFi.h>
#include <PubSubClient.h>
const char* ssid = "WiFiSSID";
const char* password = "WiFiPassword";
const char* mqtt_server = "192.168.xxx.yyy";
const char* mqtt_user = "mqtt_user";
const char* mqtt_password = "mqtt_password";
const char* mqtt_topic_maintank_ph = "aquarium/maintank/ph";
const char* mqtt_topic_maintank_ph_voltage = "aquarium/maintank/ph_voltage";
const char* mqtt_topic_esp32_heartbeat = "aquarium/others/esp32_heartbeat_sub";
// 設定GPIO
#define PH_METER_PIN 34
// 設定參考電壓
#define VREF 3.3
// 設定pH
#define pH_SampleNum 80
int pH_SampleFreq = 250;
int pH_analogBuffer[pH_SampleNum];
int pH_analogBufferIndex = 0;
float pH_Value = 7;
float pH_Voltage = 0;
float pH_slope = 0;
float pH_intercept = 0;
// 設定HeartBeat
int loop_number = 0;
WiFiClient espClient;
PubSubClient client(espClient);
void setup_wifi()
{
delay(1000);
Serial.print("Connecting to WiFi ");
Serial.print(ssid);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED)
{
delay(500);
Serial.print(".");
}
Serial.println("");
Serial.println("WiFi is connected");
Serial.print("IP address: ");
Serial.println(WiFi.localIP());
}
void setup_mqtt()
{
while (!client.connected())
{
Serial.print("Connecting to MQTT");
if (client.connect("ESP32_AquaController_sub", mqtt_user, mqtt_password))
{
Serial.println("");
Serial.println("MQTT is connected");
}
else
{
Serial.println("");
Serial.print("Failed, error code ");
Serial.print(client.state());
Serial.println(", try again in 5 seconds.");
delay(5000);
}
}
}
void callback(char* topic, byte* payload, unsigned int length)
{
payload[length] = '\0';
String message = String((char*)payload);
if (strcmp(topic, "aquarium/others/ph_slope") == 0)
{
pH_slope = message.toFloat();
}
else if (strcmp(topic, "aquarium/others/ph_intercept") == 0)
{
pH_intercept = message.toFloat();
}
}
void setup()
{
Serial.begin(115200);
pinMode(PH_METER_PIN, INPUT);
setup_wifi();
client.setServer(mqtt_server, 1883);
client.setCallback(callback);
if (!client.connected())
{
setup_mqtt();
}
client.subscribe("aquarium/others/ph_slope");
client.subscribe("aquarium/others/ph_intercept");
}
void loop()
{
if (!client.connected())
{
setup_mqtt();
}
client.loop();
//pH
static unsigned long pH_SampleTime = 0;
if(millis() - pH_SampleTime > pH_SampleFreq)
{
pH_SampleTime = millis();
pH_analogBuffer[pH_analogBufferIndex] = analogRead(PH_METER_PIN);
pH_analogBufferIndex++;
if(pH_analogBufferIndex == pH_SampleNum)
{
pH_analogBufferIndex = 0;
}
}
static unsigned long lastRPT_pH = 0;
if (millis() - lastRPT_pH > 5000)
{
lastRPT_pH = millis();
float sum = 0;
for(int i = 0; i < pH_SampleNum; i++)
{
sum += pH_analogBuffer[i];
}
pH_Voltage = (sum / pH_SampleNum) * (float)VREF / 4095;
pH_Value = pH_Voltage * pH_slope + pH_intercept;
pH_Value = round(pH_Value * 100) / 100.0;
client.publish(mqtt_topic_maintank_ph_voltage, String(pH_Voltage, 2).c_str(), true);
client.publish(mqtt_topic_maintank_ph, String(pH_Value, 2).c_str(), true);
}
// Heartbeat
static unsigned long lastRPT_HeartBeat = 0;
if (millis() - lastRPT_HeartBeat >= 60000)
{
lastRPT_HeartBeat = millis();
loop_number++;
client.publish(mqtt_topic_esp32_heartbeat, String(loop_number).c_str(), true);
}
}
用麵包板來連接可能會有鬆脫的風險,但這麼多感應器實在不容易做,先撐一段時間看看好了。