透過Home Assistant實做魚缸的監控與輔助管理之二:感應器與ESP32

因篇幅關係,拆成以下幾篇易於閱讀。

這篇先講一下怎麼把所有的感應器透過ESP32接出來,我的ESP32有二塊,分別命名為ESP32_Main與ESP32_Sub,之所以獨立出ESP32_Sub出來是因為pH模組遇到我無法解決的電路干擾問題。二塊的作業如下

這是感應器腳位與GPIO的對照表(省略GND),什麼感應器要插哪一個GPIO當然是看自己開心,但要留意一下GPIO對應ADC1或ADC2,ADC2的GPIO可能會跟Wi-Fi衝突。

ESP_MAINVCCData
主缸水溫線3.3V13 (OneWire)
補水桶水溫線3.3V13 (OneWire)
TDS模組3.3VGPIO 39
非接觸式液體感應器3.3VGPIO 35
主缸水位浮球開關GPIO 26
濾材格水位浮球開關GPIO 12
馬達格高水位浮球開關GPIO 14
馬達格低水位浮球開關GPIO 27
1602A LCD3.3V32 (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_SUBVCCData
pH5V34

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);
  }
}

用麵包板來連接可能會有鬆脫的風險,但這麼多感應器實在不容易做,先撐一段時間看看好了。

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *

返回頂端