#include <OneWire.h>
#include <DallasTemperature.h>
#include <WiFi.h>
#include <PubSubClient.h>
#include <WebServer.h>
#include <Update.h>
#include "secrets.h"
#define WIFI_CHECK_INTERVAL 30000
#define SENSOR_SAMPLE_INTERVAL 5000
#define MQTT_PUSH_INTERVAL 60000
#define SAMPLE_HISTORY_N 60 // 60*5s => 5m running average
#define PIN_ONEWIRE 4
#define PIN_TDS 34
#define VREF 3.3
float ec_calibration = 1.0f;
#define CTEMP temperatures[sensors_current_i]
#define CTDS tds[sensors_current_i]
#define CEC ec[sensors_current_i]
const char *ssid = WIFI_SSID;
const char *password = WIFI_PASSWORD;
const char *mqtt_broker = MQTT_BROKER;
const char *topic_temp = "emqx/nft_system_temp";
const char *topic_tds = "emqx/nft_system_tds";
const char *topic_ec = "emqx/nft_system_ec";
const char *mqtt_username = MQTT_USERNAME;
const char *mqtt_password = MQTT_PASSWORD;
const int mqtt_port = 1883;
WiFiClient espClient;
PubSubClient client(espClient);
OneWire oneWire(PIN_ONEWIRE);
DallasTemperature sensors(&oneWire);
int sensors_current_i = 0;
bool sensors_initialized = false;
float temperatures[SAMPLE_HISTORY_N];
float temperatures_total = 0.0f;
float temperatures_average = 0.0f;
bool temperature_mqtt_last_publish_status = false;
float tds[SAMPLE_HISTORY_N];
float tds_total = 0.0f;
float tds_average = 0.0f;
bool tds_mqtt_last_publish_status = false;
float ec[SAMPLE_HISTORY_N];
float ec_total = 0.0f;
float ec_average = 0.0f;
bool ec_mqtt_last_publish_status = false;
unsigned long lastWifiCheck = 0;
unsigned long lastSensorSample = 0;
unsigned long lastMqttPublish = 0;
WebServer server(80);
const char *serverIndex PROGMEM =
"<!DOCTYPE html>"
"<html>"
"<head><style type='text/css'>"
"div {display:flex;gap:1em;}"
"</style></head>"
"<body>"
"<div><strong>Sensors</strong><span>Initialized: <em>%d</em></span><span>Last sample: <em>%dms</em> ago</span></div><hr>"
"<div><strong>Temperature</strong><span><em>%.1fC</em> ~ <em>%.1fC</em></span><span>MQTT: <em>%d</em></span></div><hr>"
"<div><strong>TDS</strong><span><em>%.0f</em> ~ <em>%.0f</em></span><span>MQTT: <em>%d</em></span></div><hr>"
"<div><strong>EC</strong><span><em>%.1f</em> ~ <em>%.1f</em></span><span>MQTT: <em>%d</em></span></div><hr>"
"<div><strong>MQTT</strong><span>Last <em>%dms</em> ago</span></div><hr>"
"<form method='POST' action='/update' enctype='multipart/form-data'><input type='file' name='update'><input type='submit' value='Update'></form>"
"</body>"
"</html>";
void setup_wifi() {
delay(3);
Serial.println();
Serial.print("Connecting to ");
Serial.println(ssid);
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
randomSeed(micros());
Serial.println("");
Serial.println("WiFi connected");
Serial.println("IP address: ");
Serial.println(WiFi.localIP());
}
void reconnect() {
while (!client.connected()) {
Serial.print("Attempting MQTT connection...");
String client_id = "esp32-client-";
client_id += String(WiFi.macAddress());
if (client.connect(client_id.c_str(), mqtt_username, mqtt_password)) {
Serial.println("connected");
} else {
Serial.print("failed, rc=");
Serial.print(client.state());
Serial.println(" try again in 5 seconds");
delay(5000);
}
}
}
void initialize_sensors() {
sensors.begin();
pinMode(PIN_TDS, INPUT);
for (int i = 0; i < SAMPLE_HISTORY_N; i++) {
temperatures[i] = 0.0f;
}
}
void setup() {
Serial.begin(115200);
Serial.println("init");
initialize_sensors();
setup_wifi();
client.setServer(mqtt_broker, mqtt_port);
server.on("/", HTTP_GET, []() {
unsigned long now = millis();
server.sendHeader("Connection", "close");
char buffer[strlen(serverIndex)+64];
sprintf(
buffer, serverIndex,
sensors_initialized, now - lastSensorSample,
CTEMP, temperatures_average, temperature_mqtt_last_publish_status,
CTDS, tds_average, tds_mqtt_last_publish_status,
CEC, ec_average, ec_mqtt_last_publish_status,
now - lastMqttPublish
);
server.send(200, "text/html", buffer);
});
server.on(
"/update", HTTP_POST,
[]() {
server.sendHeader("Connection", "close");
server.send(200, "text/plain", (Update.hasError()) ? "FAIL" : "OK");
ESP.restart();
},
[]() {
HTTPUpload &upload = server.upload();
if (upload.status == UPLOAD_FILE_START) {
Serial.setDebugOutput(true);
Serial.printf("Update: %s\n", upload.filename.c_str());
if (!Update.begin()) { //start with max available size
Update.printError(Serial);
}
} else if (upload.status == UPLOAD_FILE_WRITE) {
if (Update.write(upload.buf, upload.currentSize) != upload.currentSize) {
Update.printError(Serial);
}
} else if (upload.status == UPLOAD_FILE_END) {
if (Update.end(true)) { //true to set the size to the current progress
Serial.printf("Update Success: %u\nRebooting...\n", upload.totalSize);
} else {
Update.printError(Serial);
}
Serial.setDebugOutput(false);
} else {
Serial.printf(F("Update Failed Unexpectedly (likely broken connection): status=%d\n"), upload.status);
}
}
);
server.begin();
}
void updateSensorValues() {
sensors.requestTemperatures();
sensors_current_i++;
if (sensors_current_i >= SAMPLE_HISTORY_N) {
sensors_current_i = 0;
sensors_initialized = true;
}
// Temperature 1
temperatures_total -= CTEMP;
CTEMP = sensors.getTempCByIndex(0);
temperatures_total += CTEMP;
temperatures_average = temperatures_total / SAMPLE_HISTORY_N;
// TDS 1/EC 1
ec_total -= CEC;
tds_total -= CTDS;
float rawEc = analogRead(PIN_TDS) * VREF / 4096.0; // read the analog value more stable by the median filtering algorithm, and convert to voltage value
float temperatureCoefficient = 1.0 + 0.02 * (temperatures_average - 25.0); // temperature compensation formula: fFinalResult(25^C) = fFinalResult(current)/(1.0+0.02*(fTP-25.0));
CEC = (rawEc / temperatureCoefficient) * ec_calibration; // temperature and calibration compensation
ec_total += CEC;
ec_average = ec_total / SAMPLE_HISTORY_N;
CTDS = (133.42 * pow(CEC, 3) - 255.86 * CEC * CEC + 857.39 * CEC) * 0.5; //convert voltage value to tds value
tds_total += CTDS;
tds_average = tds_total / SAMPLE_HISTORY_N;
Serial.print(F("TDS:")); Serial.println(CTDS);
Serial.print(F("EC:")); Serial.println(CEC, 2);
Serial.print(F(" "));
Serial.print(CTEMP);
Serial.print(F(" C"));
Serial.print(F("~"));
Serial.print(temperatures_average);
Serial.println(F(" C"));
}
void loop() {
unsigned long now = millis();
if ((WiFi.status() != WL_CONNECTED) && (now - lastWifiCheck > WIFI_CHECK_INTERVAL)) {
Serial.println("Reconnecting to WiFi...");
WiFi.disconnect();
WiFi.reconnect();
lastWifiCheck = now;
}
server.handleClient();
if (!client.connected()) {
reconnect();
}
client.loop();
now = millis();
if (now - lastSensorSample > SENSOR_SAMPLE_INTERVAL) {
lastSensorSample = now;
updateSensorValues();
}
if (now - lastMqttPublish > MQTT_PUSH_INTERVAL) {
lastMqttPublish = now;
if (sensors_initialized) {
// Publish temperature
char result[8]; // Buffer big enough for 7-character float
dtostrf(temperatures_average, 6, 1, result); // Leave room for too large numbers!
temperature_mqtt_last_publish_status = client.publish(topic_temp, result);
Serial.println("Published temperature update");
// Publish tds
result[8]; // Buffer big enough for 7-character float
dtostrf(tds_average, 6, 0, result); // Leave room for too large numbers!
tds_mqtt_last_publish_status = client.publish(topic_tds, result);
Serial.println("Published tds update");
// Publish ec
result[8]; // Buffer big enough for 7-character float
dtostrf(ec_average, 6, 1, result); // Leave room for too large numbers!
ec_mqtt_last_publish_status = client.publish(topic_ec, result);
Serial.println("Published ec update");
}
}
}
// median filtering algorithm
// https://randomnerdtutorials.com/arduino-tds-water-quality-sensor/
int getMedianNum(int bArray[], int iFilterLen){
int bTab[iFilterLen];
for (byte i = 0; i<iFilterLen; i++)
bTab[i] = bArray[i];
int i, j, bTemp;
for (j = 0; j < iFilterLen - 1; j++) {
for (i = 0; i < iFilterLen - j - 1; i++) {
if (bTab[i] > bTab[i + 1]) {
bTemp = bTab[i];
bTab[i] = bTab[i + 1];
bTab[i + 1] = bTemp;
}
}
}
if ((iFilterLen & 1) > 0){
bTemp = bTab[(iFilterLen - 1) / 2];
}
else {
bTemp = (bTab[iFilterLen / 2] + bTab[iFilterLen / 2 - 1]) / 2;
}
return bTemp;
}