#include <Arduino.h>
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <OneWire.h>
#include <DallasTemperature.h>
#include <WiFi.h>
#define MQTT_MAX_PACKET_SIZE 2048
#include <PubSubClient.h>
#include <NTPClient.h>
#include <WiFiUdp.h>
#include <ArduinoJson.h>
#include <EEPROM.h>
// LCD 2004 I2C address
LiquidCrystal_I2C lcd(0x27, 20, 4);
// Ultrasonic sensor 1 pins (cũ)
const int TRIG_PIN = 19;
const int ECHO_PIN = 18;
// Ultrasonic sensor 2 pins (mới)
const int TRIG_PIN2 = 16;
const int ECHO_PIN2 = 17;
// DS18B20 temperature sensor on D4
#define ONE_WIRE_BUS 4
OneWire oneWire(ONE_WIRE_BUS);
DallasTemperature sensors(&oneWire);
void sendSensorData();
// pH & TDS
const int PH_PIN = 32;
const int TDS_PIN = 33;
// Van dinh duong
const int VAN_DINH_DUONG_PIN = 13;
bool vanDinhDuongState = false;
// ===== Máy bơm =====
const int RL_EN = 14; // PWM speed control
const int R_PWM = 27; // Direction control
const int L_PWM = 23; // Direction control
int pumpPwmValue = 0; // Tốc độ hiện tại của máy bơm (0-255)
// ==== Bộ lọc thô cho cảm biến ====
const float ULTRA1_LIMIT[2] = {0.0, 20.0}; // cm
const float ULTRA2_LIMIT[2] = {0.0, 20.0}; // cm
const float TEMP_LIMIT[2] = {0.0, 60.0}; // °C
const float PH_LIMIT[2] = {0.0, 14.0}; // pH
const float TDS_LIMIT[2] = {0.0, 3000.0}; // ppm
// ===== Động cơ khuấy và Đèn =====
const int STIR_PIN = 25;
const int LIGHT_PIN = 26;
bool stirState = false;
bool lightState = false;
int stirPwmValue = 0; // Tốc độ hiện tại của động cơ khuấy (0-255)
// WiFi & MQTT
struct WifiConfig {
const char* ssid;
const char* password;
};
WifiConfig wifiList[] = {
{"M Coffee", "1234567890"},
{"Wokwi-GUEST", ""},
};
const int wifiListSize = sizeof(wifiList) / sizeof(wifiList[0]);
int wifiConnectedIndex = -1; // Chỉ số wifi đã kết nối (nếu thành công)
const char* mqtt_server = "103.146.22.13";
const char* mqtt_user = "user1";
const char* mqtt_pass = "12345678";
const char* mqtt_topic = "doan/thuycanh/sensor";
const char* mqtt_control_topic = "doan/thuycanh/control";
const char* mqtt_auto_topic = "doan/thuycanh/auto";
const char* mqtt_schedule_topic = "doan/thuycanh/schedule";
// NTP Client
WiFiUDP ntpUDP;
NTPClient timeClient(ntpUDP, "pool.ntp.org", 7 * 3600, 60000);
WiFiClient espClient;
PubSubClient client(espClient);
long duration;
float distance;
// Thêm cho cảm biến siêu âm thứ 2
long duration2;
float distance2;
unsigned long lastSensorSent = 0;
const unsigned long sensorSendInterval = 10000; // ms
unsigned long lastTimeDisplay = 0;
const unsigned long timeDisplayInterval = 1000; // ms, update thời gian lên màn hình mỗi 1s
// ========================== EEPROM for AUTO ==========================
#define EEPROM_SIZE 2048
#define AUTO_RULES_MAX 10
struct AutoRule {
uint8_t id;
char sensor[8];
char operator_[3];
float value;
char target[16];
char action[4];
};
AutoRule autoRules[AUTO_RULES_MAX];
uint8_t autoRuleCount = 0;
// ========================== EEPROM for SCHEDULE ==========================
#define SCHEDULE_EEPROM_ADDR 100
struct ScheduleRule {
char type[8]; // "once", "daily", "weekly"
char date[11]; // "yyyy-mm-dd" cho once
uint8_t days[7]; // cho weekly, các giá trị từ 1-7
uint8_t daysCount; // số lượng ngày thực sự
char time[6]; // "hh:mm"
char target[16]; // "van_dinh_duong", "stir", "light"
char action[8]; // "on", "off"
};
ScheduleRule scheduleRule;
bool hasSchedule = false;
// -------------------- EEPROM Helpers --------------------
// AUTO
void loadAutoRulesFromEEPROM() {
autoRuleCount = EEPROM.read(0);
if (autoRuleCount > 1) autoRuleCount = 0;
if (autoRuleCount == 1) {
int addr = 1;
EEPROM.get(addr, autoRules[0]);
}
}
void saveAutoRulesToEEPROM() {
EEPROM.write(0, autoRuleCount);
if (autoRuleCount == 1) {
int addr = 1;
EEPROM.put(addr, autoRules[0]);
}
EEPROM.commit();
}
// SCHEDULE
void loadScheduleFromEEPROM() {
hasSchedule = EEPROM.read(SCHEDULE_EEPROM_ADDR);
if (hasSchedule) {
int addr = SCHEDULE_EEPROM_ADDR + 1;
EEPROM.get(addr, scheduleRule);
}
}
void saveScheduleToEEPROM() {
EEPROM.write(SCHEDULE_EEPROM_ADDR, hasSchedule ? 1 : 0);
if (hasSchedule) {
int addr = SCHEDULE_EEPROM_ADDR + 1;
EEPROM.put(addr, scheduleRule);
}
EEPROM.commit();
}
// -------------------- PUBLISH --------------------
void publishAutoRules() {
timeClient.update();
time_t rawtime = timeClient.getEpochTime();
struct tm * timeinfo = localtime(&rawtime);
char dateBuf[24];
snprintf(dateBuf, sizeof(dateBuf), "%04d-%02d-%02d %02d:%02d:%02d",
timeinfo->tm_year + 1900, timeinfo->tm_mon + 1, timeinfo->tm_mday,
timeinfo->tm_hour, timeinfo->tm_min, timeinfo->tm_sec);
StaticJsonDocument<512> doc;
JsonArray arr = doc.to<JsonArray>();
if (autoRuleCount > 0) {
JsonObject obj = arr.createNestedObject();
obj["index"] = 1;
obj["sensor"] = autoRules[0].sensor;
obj["operator"] = autoRules[0].operator_;
obj["value"] = autoRules[0].value;
obj["target"] = autoRules[0].target;
obj["action"] = autoRules[0].action;
obj["time"] = dateBuf;
}
String jsonStr;
serializeJson(arr, jsonStr);
Serial.print("[AUTO] Publish: ");
Serial.println(jsonStr);
client.publish(mqtt_auto_topic, jsonStr.c_str());
}
void publishSchedule() {
StaticJsonDocument<512> doc;
JsonArray arr = doc.to<JsonArray>();
if (hasSchedule) {
JsonObject obj = arr.createNestedObject();
obj["type"] = scheduleRule.type;
obj["time"] = scheduleRule.time;
obj["target"] = scheduleRule.target;
obj["action"] = scheduleRule.action;
if (strcmp(scheduleRule.type, "once") == 0) {
obj["date"] = scheduleRule.date;
} else if (strcmp(scheduleRule.type, "weekly") == 0) {
JsonArray daysArr = obj.createNestedArray("days");
for (uint8_t i = 0; i < scheduleRule.daysCount; i++) {
daysArr.add(scheduleRule.days[i]);
}
}
}
String jsonStr;
serializeJson(arr, jsonStr);
Serial.print("[SCHEDULE] Publish: ");
Serial.println(jsonStr);
client.publish(mqtt_schedule_topic, jsonStr.c_str());
}
// -------------------- RESET --------------------
void handleResetAutoRules() {
autoRuleCount = 0;
saveAutoRulesToEEPROM();
publishAutoRules();
Serial.println("[AUTO] Reset all auto rules!");
}
void handleResetSchedule() {
hasSchedule = false;
saveScheduleToEEPROM();
publishSchedule();
Serial.println("[SCHEDULE] Reset all schedule!");
}
// -------------------- HANDLERS --------------------
void handleAddAuto(const JsonObject& obj) {
AutoRule r;
r.id = 1;
strlcpy(r.sensor, obj["sensor"] | "", sizeof(r.sensor));
strlcpy(r.operator_, obj["operator"] | "", sizeof(r.operator_));
r.value = obj["value"] | 0;
strlcpy(r.target, obj["target"] | "", sizeof(r.target));
strlcpy(r.action, obj["action"] | "", sizeof(r.action));
autoRuleCount = 1;
autoRules[0] = r;
saveAutoRulesToEEPROM();
publishAutoRules();
Serial.println("[AUTO] Added rule (overwrite, only 1 allowed)");
}
void handleDeleteAuto(uint8_t idx) {
if (idx != 1 || autoRuleCount == 0) {
Serial.println("[AUTO] Delete: Invalid index");
return;
}
autoRuleCount = 0;
saveAutoRulesToEEPROM();
publishAutoRules();
Serial.println("[AUTO] Deleted rule at idx 1");
}
void handleAddSchedule(const JsonObject& obj) {
strlcpy(scheduleRule.type, obj["type"] | "", sizeof(scheduleRule.type));
strlcpy(scheduleRule.target, obj["target"] | "", sizeof(scheduleRule.target));
strlcpy(scheduleRule.action, obj["action"] | "", sizeof(scheduleRule.action));
scheduleRule.daysCount = 0;
memset(scheduleRule.days, 0, sizeof(scheduleRule.days));
scheduleRule.date[0] = 0;
scheduleRule.time[0] = 0;
if (strcmp(scheduleRule.type, "once") == 0) {
// Nhận chuỗi time kiểu "yyyy-mm-dd HH:mm"
String fullTime = obj["time"] | "";
if (fullTime.length() == 16 && fullTime.charAt(10) == ' ') {
// yyyy-mm-dd HH:mm
strlcpy(scheduleRule.date, fullTime.substring(0, 10).c_str(), sizeof(scheduleRule.date));
strlcpy(scheduleRule.time, fullTime.substring(11, 16).c_str(), sizeof(scheduleRule.time));
} else if (fullTime.length() == 5 && fullTime.charAt(2) == ':') {
// chỉ HH:mm (lỡ gửi sai vẫn lấy được)
scheduleRule.date[0] = 0;
strlcpy(scheduleRule.time, fullTime.c_str(), sizeof(scheduleRule.time));
} else {
// fallback
strlcpy(scheduleRule.time, fullTime.c_str(), sizeof(scheduleRule.time));
scheduleRule.date[0] = 0;
}
} else if (strcmp(scheduleRule.type, "daily") == 0) {
strlcpy(scheduleRule.time, obj["time"] | "", sizeof(scheduleRule.time));
} else if (strcmp(scheduleRule.type, "weekly") == 0) {
strlcpy(scheduleRule.time, obj["time"] | "", sizeof(scheduleRule.time));
if (obj.containsKey("days")) {
JsonArray arr = obj["days"].as<JsonArray>();
scheduleRule.daysCount = arr.size();
for (int i = 0; i < arr.size() && i < 7; i++)
scheduleRule.days[i] = arr[i];
}
}
hasSchedule = true;
saveScheduleToEEPROM();
publishSchedule();
Serial.println("[SCHEDULE] Added schedule (overwrite, only 1 allowed)");
}
void handleDeleteSchedule(uint8_t idx) {
if (idx != 1 || !hasSchedule) {
Serial.println("[SCHEDULE] Delete: Invalid index");
return;
}
hasSchedule = false;
saveScheduleToEEPROM();
publishSchedule();
Serial.println("[SCHEDULE] Deleted schedule at idx 1");
}
// -------------------- MQTT Callback --------------------
void mqttCallback(char* topic, byte* message, unsigned int length) {
message[length] = '\0';
String msgStr = String((char*)message);
Serial.print("[MQTT] Topic: ");
Serial.println(topic);
Serial.print("[MQTT] Payload: ");
Serial.println(msgStr);
StaticJsonDocument<256> doc;
DeserializationError error = deserializeJson(doc, msgStr);
if (!error) {
// --- handle reset auto
if (doc.containsKey("reset")) {
bool doReset = doc["reset"];
if (doReset) {
handleResetAutoRules();
return;
}
}
// --- handle reset schedule
if (doc.containsKey("reset_schedule")) {
bool doReset = doc["reset_schedule"];
if (doReset) {
handleResetSchedule();
return;
}
}
// --- handle add_auto
if (doc.containsKey("add_auto")) {
JsonObject obj = doc["add_auto"].as<JsonObject>();
handleAddAuto(obj);
return;
}
// --- handle delete_auto
if (doc.containsKey("delete_auto")) {
int idx = doc["delete_auto"];
handleDeleteAuto((uint8_t)idx);
return;
}
// --- handle add_schedule
if (doc.containsKey("add_schedule")) {
JsonObject obj = doc["add_schedule"].as<JsonObject>();
handleAddSchedule(obj);
return;
}
// --- handle delete_schedule
if (doc.containsKey("delete_schedule")) {
int idx = doc["delete_schedule"];
handleDeleteSchedule((uint8_t)idx);
return;
}
// --- handle get_autos or get_schedules
if (doc.containsKey("cmd")) {
String cmd = doc["cmd"];
if (cmd == "get_autos") {
publishAutoRules();
Serial.println("[AUTO] Sent current auto rules to MQTT");
return;
}
if (cmd == "get_schedules") {
publishSchedule();
Serial.println("[SCHEDULE] Sent current schedule to MQTT");
return;
}
}
// Điều khiển van dinh dưỡng
if (doc.containsKey("van_dinh_duong")) {
String cmd = doc["van_dinh_duong"];
Serial.print("[MQTT] van_dinh_duong: ");
Serial.println(cmd);
if (cmd == "on") {
digitalWrite(VAN_DINH_DUONG_PIN, HIGH);
vanDinhDuongState = true;
lcd.setCursor(0,0); lcd.print("Van dinh duong: BAT ");
Serial.println("[LCD] Van dinh duong: BAT");
} else if (cmd == "off") {
digitalWrite(VAN_DINH_DUONG_PIN, LOW);
vanDinhDuongState = false;
lcd.setCursor(0,0); lcd.print("Van dinh duong: TAT ");
Serial.println("[LCD] Van dinh duong: TAT");
}
sendSensorData();
}
// Điều khiển máy bơm
if (doc.containsKey("pump")) {
int pwmValue = doc["pump"];
Serial.print("[MQTT] pump: ");
Serial.println(pwmValue);
if (pwmValue < 0) pwmValue = 0;
if (pwmValue > 255) pwmValue = 255;
pumpPwmValue = pwmValue;
// Wokwi không hỗ trợ ledcWrite, thay bằng digitalWrite để mô phỏng đơn giản (chỉ HIGH/LOW)
if (pumpPwmValue > 127) {
digitalWrite(RL_EN, HIGH);
} else {
digitalWrite(RL_EN, LOW);
}
lcd.setCursor(0,1); lcd.print("Bom: "); lcd.print(pumpPwmValue); lcd.print(" ");
Serial.print("[LCD] Bom: "); Serial.println(pumpPwmValue);
sendSensorData();
}
// Điều khiển động cơ khuấy (nhận PWM hoặc on/off)
if (doc.containsKey("stir")) {
if (doc["stir"].is<int>()) {
// Nhận giá trị PWM
int pwmValue = doc["stir"];
Serial.print("[MQTT] stir PWM: ");
Serial.println(pwmValue);
if (pwmValue < 0) pwmValue = 0;
if (pwmValue > 255) pwmValue = 255;
stirPwmValue = pwmValue;
if (stirPwmValue > 127) {
digitalWrite(STIR_PIN, HIGH);
} else {
digitalWrite(STIR_PIN, LOW);
}
stirState = (stirPwmValue > 0);
lcd.setCursor(0,2); lcd.print("Khuay: "); lcd.print(stirPwmValue); lcd.print(" ");
Serial.print("[LCD] Khuay: "); Serial.println(stirPwmValue);
} else {
String cmd = doc["stir"];
Serial.print("[MQTT] stir: ");
Serial.println(cmd);
if (cmd == "on") {
stirPwmValue = 255;
digitalWrite(STIR_PIN, HIGH);
stirState = true;
lcd.setCursor(0,2); lcd.print("Khuay: BAT ");
Serial.println("[LCD] Khuay: BAT");
} else if (cmd == "off") {
stirPwmValue = 0;
digitalWrite(STIR_PIN, LOW);
stirState = false;
lcd.setCursor(0,2); lcd.print("Khuay: TAT ");
Serial.println("[LCD] Khuay: TAT");
}
}
sendSensorData();
}
// Điều khiển đèn
if (doc.containsKey("light")) {
String cmd = doc["light"];
Serial.print("[MQTT] light: ");
Serial.println(cmd);
if (cmd == "on") {
digitalWrite(LIGHT_PIN, HIGH);
lightState = true;
lcd.setCursor(0,3); lcd.print("Den: BAT ");
Serial.println("[LCD] Den: BAT");
} else if (cmd == "off") {
digitalWrite(LIGHT_PIN, LOW);
lightState = false;
lcd.setCursor(0,3); lcd.print("Den: TAT ");
Serial.println("[LCD] Den: TAT");
}
sendSensorData();
}
} else {
Serial.println("[MQTT] JSON Parse Error!");
}
}
// =================== WiFi đa mạng và hiển thị LCD ======================
void connectWiFi() {
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Dang ket noi WiFi");
Serial.println("[WiFi] Dang ket noi WiFi");
WiFi.mode(WIFI_STA);
wifiConnectedIndex = -1;
int retry = 0;
for (int i = 0; i < wifiListSize; ++i) {
WiFi.begin(wifiList[i].ssid, wifiList[i].password);
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Thu WiFi: ");
lcd.print(wifiList[i].ssid);
Serial.print("[WiFi] Thu WiFi: ");
Serial.println(wifiList[i].ssid);
retry = 0;
while (WiFi.status() != WL_CONNECTED && retry < 12) { // thử 6s cho mỗi wifi
delay(500);
lcd.print(".");
Serial.print(".");
retry++;
}
if (WiFi.status() == WL_CONNECTED) {
wifiConnectedIndex = i;
break;
}
}
lcd.clear();
if (wifiConnectedIndex != -1) {
lcd.setCursor(0, 0);
lcd.print("WiFi OK: ");
lcd.print(WiFi.localIP());
lcd.setCursor(0, 1);
lcd.print("SSID: ");
lcd.print(wifiList[wifiConnectedIndex].ssid);
Serial.print("[WiFi] OK: ");
Serial.println(WiFi.localIP());
Serial.print("[WiFi] Da ket noi SSID: ");
Serial.println(wifiList[wifiConnectedIndex].ssid);
delay(2000); // Để cho người dùng xem tên wifi đã kết nối
} else {
lcd.setCursor(0, 0);
lcd.print("WiFi FAIL");
Serial.println("[WiFi] FAIL");
delay(2000);
}
}
void connectMQTT() {
while (!client.connected()) {
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("MQTT...");
Serial.println("[MQTT] Dang ket noi MQTT...");
if (client.connect("ESP32Client", mqtt_user, mqtt_pass)) {
lcd.setCursor(0, 1);
lcd.print("Da ket noi MQTT");
Serial.println("[MQTT] Da ket noi MQTT");
delay(1000);
client.subscribe(mqtt_control_topic); // Subscribe topic điều khiển mỗi khi kết nối lại
Serial.print("[MQTT] Subscribed: ");
Serial.println(mqtt_control_topic);
lcd.clear();
} else {
lcd.setCursor(0, 1);
lcd.print("Fail. Wait retry");
Serial.println("[MQTT] Fail. Wait retry");
delay(2000);
}
}
}
// ==== Hàm lọc thô giá trị cảm biến ====
template <typename T>
T filterRaw(T value, const float limit[2], T invalid = -9999) {
if (value < limit[0] || value > limit[1]) return invalid;
return value;
}
// Đo siêu âm 1
float readUltrasonic() {
digitalWrite(TRIG_PIN, LOW);
delayMicroseconds(2);
digitalWrite(TRIG_PIN, HIGH);
delayMicroseconds(10);
digitalWrite(TRIG_PIN, LOW);
duration = pulseIn(ECHO_PIN, HIGH, 30000);
distance = duration * 0.0343 / 2;
// Lọc thô
distance = filterRaw(distance, ULTRA1_LIMIT, -1.0f);
if (duration == 0) return -1;
return distance;
}
// Đo siêu âm 2 (chân 16, 17)
float readUltrasonic2() {
digitalWrite(TRIG_PIN2, LOW);
delayMicroseconds(2);
digitalWrite(TRIG_PIN2, HIGH);
delayMicroseconds(10);
digitalWrite(TRIG_PIN2, LOW);
duration2 = pulseIn(ECHO_PIN2, HIGH, 30000);
distance2 = duration2 * 0.0343 / 2;
// Lọc thô
distance2 = filterRaw(distance2, ULTRA2_LIMIT, -1.0f);
if (duration2 == 0) return -1;
return distance2;
}
float readTemperature() {
sensors.requestTemperatures();
float t = sensors.getTempCByIndex(0);
return filterRaw(t, TEMP_LIMIT, -1.0f);
}
float readPH() {
int adcValue = analogRead(PH_PIN);
float voltage = adcValue * (3.3 / 4095.0);
// Lọc thô
return filterRaw(voltage, PH_LIMIT, -1.0f);
}
float readTDS(float temperatureC) {
int adcValue = analogRead(TDS_PIN);
float voltage = adcValue * (3.3 / 4095.0);
float compensationCoefficient = 1.0 + 0.02 * (temperatureC - 25.0);
float compensationVoltage = voltage / compensationCoefficient;
float tdsValue = (133.42 * compensationVoltage * compensationVoltage * compensationVoltage
- 255.86 * compensationVoltage * compensationVoltage
+ 857.39 * compensationVoltage) * 0.5;
// Lọc thô
return filterRaw(tdsValue, TDS_LIMIT, -1.0f);
}
void displayTimeOnly() {
// Hiển thị thời gian lên dòng đầu mỗi 1s, không xóa màn hình
String timeStr = timeClient.getFormattedTime();
lcd.setCursor(5, 0); // Đặt ở giữa dòng 0 (sát phải chữ "Van dinh duong" nếu đang ở màn hình sensor)
lcd.print(timeStr);
}
void sendSensorData() {
float d = 11 - readUltrasonic(); // Mực nước cảm biến 1 (giả sử bể cao 11cm)
float d2 = 10- readUltrasonic2(); // Mực nước cảm biến 2 (giả sử bể cao 8cm)
float t = readTemperature();
float ph = readPH();
float tds = readTDS(t);
String timeStr = timeClient.getFormattedTime();
lcd.clear();
lcd.setCursor(5, 0);
lcd.print(timeStr);
lcd.setCursor(0, 1);
lcd.print("MN1: ");
if (d < 0) lcd.print(0, 1);
else lcd.print(d, 1);
lcd.print("cm ");
lcd.setCursor(10, 1);
lcd.print("MN2: ");
if (d2 < 0) lcd.print(0, 1);
else lcd.print(d2, 1);
lcd.print("cm");
lcd.setCursor(0, 2);
lcd.print("TDS: ");
if (tds < 0) lcd.print("Loi");
else {
lcd.print(tds, 2);
lcd.print(" ppm");
}
lcd.setCursor(0, 3);
lcd.print("pH: ");
lcd.print(ph, 2);
lcd.print(" ");
lcd.print("ND: ");
lcd.print(t, 1);
lcd.print("C");
Serial.print("[SENSOR] ");
Serial.print(timeStr);
Serial.print(" | MN1: ");
Serial.print(d, 1); Serial.print("cm");
Serial.print(" | MN2: ");
Serial.print(d2, 1); Serial.print("cm");
Serial.print(" | TDS: ");
Serial.print(tds, 2); Serial.print("ppm");
Serial.print(" | pH: ");
Serial.print(ph, 2);
Serial.print(" | Nhiet do: ");
Serial.print(t, 1); Serial.println("C");
Serial.print("[STATE] Van: ");
Serial.print(vanDinhDuongState ? "BAT" : "TAT");
Serial.print(" | Bom: ");
Serial.print(pumpPwmValue);
Serial.print(" | Khuay: ");
Serial.print(stirPwmValue);
Serial.print(" | Den: ");
Serial.println(lightState ? "BAT" : "TAT");
char payload[300];
snprintf(payload, sizeof(payload),
"{\"distance\":%.2f,\"distance2\":%.2f,\"temp\":%.2f,\"ph\":%.2f,\"tds\":%.2f,"
"\"time\":\"%s\",\"van_dinh_duong\":%s,\"pump\":%d,"
"\"stir\":%d,\"light\":%s}",
d, d2, t, ph, tds, timeStr.c_str(),
vanDinhDuongState ? "true" : "false",
pumpPwmValue,
stirPwmValue,
lightState ? "true" : "false"
);
client.publish(mqtt_topic, payload);
Serial.print("[MQTT] Publish: "); Serial.println(payload);
}
void setup() {
Serial.begin(9600);
EEPROM.begin(EEPROM_SIZE);
loadAutoRulesFromEEPROM();
loadScheduleFromEEPROM();
pinMode(TRIG_PIN, OUTPUT);
pinMode(ECHO_PIN, INPUT);
pinMode(TRIG_PIN2, OUTPUT);
pinMode(ECHO_PIN2, INPUT);
pinMode(PH_PIN, INPUT);
pinMode(TDS_PIN, INPUT);
pinMode(VAN_DINH_DUONG_PIN, OUTPUT);
digitalWrite(VAN_DINH_DUONG_PIN, LOW);
pinMode(RL_EN, OUTPUT);
pinMode(R_PWM, OUTPUT);
pinMode(L_PWM, OUTPUT);
digitalWrite(R_PWM, HIGH);
digitalWrite(L_PWM, LOW);
digitalWrite(RL_EN, LOW);
// PWM cho động cơ khuấy
pinMode(STIR_PIN, OUTPUT);
digitalWrite(STIR_PIN, LOW);
pinMode(LIGHT_PIN, OUTPUT);
digitalWrite(LIGHT_PIN, LOW);
lcd.init();
lcd.backlight();
lcd.setCursor(0, 0);
lcd.print("Khoi dong...");
Serial.println("[SYSTEM] Khoi dong...");
sensors.begin();
connectWiFi();
timeClient.begin();
client.setServer(mqtt_server, 1883);
client.setCallback(mqttCallback);
connectMQTT();
publishAutoRules();
publishSchedule();
}
void loop() {
if (WiFi.status() != WL_CONNECTED)
connectWiFi();
if (!client.connected())
connectMQTT();
client.loop();
timeClient.update();
unsigned long now = millis();
if (now - lastSensorSent >= sensorSendInterval) {
lastSensorSent = now;
sendSensorData();
}
// Update thời gian lên màn hình mỗi 1s (không xóa các dòng khác)
if (now - lastTimeDisplay >= timeDisplayInterval) {
lastTimeDisplay = now;
displayTimeOnly();
}
}