first commit
This commit is contained in:
commit
0179a8063e
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,415 @@
|
|||
// ========== LIBRARY ==========
|
||||
#include <WiFi.h>
|
||||
#include <PubSubClient.h>
|
||||
#include <OneWire.h>
|
||||
#include <DallasTemperature.h>
|
||||
#include <ArduinoJson.h>
|
||||
#include <time.h>
|
||||
#include <EEPROM.h>
|
||||
#include <WiFiManager.h>
|
||||
#include <math.h>
|
||||
|
||||
// ========== KONFIGURASI ==========
|
||||
const char* mqtt_server = "broker.emqx.io";
|
||||
const int mqtt_port = 1883;
|
||||
const char* ntpServer = "pool.ntp.org";
|
||||
const long gmtOffset_sec = 7 * 3600;
|
||||
const int daylightOffset_sec = 0;
|
||||
|
||||
const char* topic_data = "aquamonn/data";
|
||||
const char* topic_relay = "aquamonn/relay";
|
||||
const char* topic_relay_status = "aquamonn/relay_status";
|
||||
const char* topic_lampu = "aquamonn/lampu";
|
||||
const char* topic_schedule = "aquamonn/schedule";
|
||||
const char* topic_lampu_status = "aquamonn/lampu_status";
|
||||
|
||||
#define PH_SENSOR_PIN 34
|
||||
#define ONE_WIRE_BUS 23
|
||||
#define RELAY_LAMPU 25
|
||||
#define RELAY_POMPA 26
|
||||
|
||||
#define EEPROM_SIZE 64
|
||||
#define EEPROM_MAGIC_NUMBER 0xA5
|
||||
#define EEPROM_ADDR_MAGIC 0
|
||||
#define EEPROM_ADDR_AUTO_MODE 1
|
||||
#define EEPROM_ADDR_ON_HOUR 2
|
||||
#define EEPROM_ADDR_ON_MINUTE 3
|
||||
#define EEPROM_ADDR_OFF_HOUR 4
|
||||
#define EEPROM_ADDR_OFF_MINUTE 5
|
||||
|
||||
#define INTERVAL_SENSOR 2000
|
||||
#define INTERVAL_NTP_SYNC 3600000
|
||||
|
||||
// Kalibrasi pH
|
||||
float V0 = 3.15, V7 = 2.60, V10 = 2.06;
|
||||
|
||||
// ========== OBJEK ==========
|
||||
OneWire oneWire(ONE_WIRE_BUS);
|
||||
DallasTemperature sensors(&oneWire);
|
||||
WiFiClient espClient;
|
||||
PubSubClient client(espClient);
|
||||
|
||||
// ========== VARIABEL ==========
|
||||
float suhu = 25.0, pH = 7.0, amonia = 0.0;
|
||||
float voltPh = 0.0;
|
||||
bool pompaAktif = false, lampuAktif = false, pompaAutoMode = true;
|
||||
float batasAmonia = 2.0;
|
||||
bool lampuAutoMode = true;
|
||||
int lampuOnHour = 6, lampuOnMinute = 0, lampuOffHour = 18, lampuOffMinute = 0;
|
||||
unsigned long lastSensorRead = 0, lastMQTT = 0, lastNTPSync = 0;
|
||||
bool ntpSynced = false, scheduleChanged = false;
|
||||
unsigned long lastScheduleChange = 0;
|
||||
|
||||
// ========== EEPROM ==========
|
||||
void saveToEEPROM() {
|
||||
EEPROM.begin(EEPROM_SIZE);
|
||||
EEPROM.write(EEPROM_ADDR_MAGIC, EEPROM_MAGIC_NUMBER);
|
||||
EEPROM.write(EEPROM_ADDR_AUTO_MODE, lampuAutoMode ? 1 : 0);
|
||||
EEPROM.write(EEPROM_ADDR_ON_HOUR, lampuOnHour);
|
||||
EEPROM.write(EEPROM_ADDR_ON_MINUTE, lampuOnMinute);
|
||||
EEPROM.write(EEPROM_ADDR_OFF_HOUR, lampuOffHour);
|
||||
EEPROM.write(EEPROM_ADDR_OFF_MINUTE, lampuOffMinute);
|
||||
EEPROM.commit();
|
||||
EEPROM.end();
|
||||
Serial.println("✅ Jadwal tersimpan");
|
||||
}
|
||||
|
||||
void loadFromEEPROM() {
|
||||
EEPROM.begin(EEPROM_SIZE);
|
||||
if (EEPROM.read(EEPROM_ADDR_MAGIC) == EEPROM_MAGIC_NUMBER) {
|
||||
lampuAutoMode = EEPROM.read(EEPROM_ADDR_AUTO_MODE);
|
||||
lampuOnHour = EEPROM.read(EEPROM_ADDR_ON_HOUR);
|
||||
lampuOnMinute = EEPROM.read(EEPROM_ADDR_ON_MINUTE);
|
||||
lampuOffHour = EEPROM.read(EEPROM_ADDR_OFF_HOUR);
|
||||
lampuOffMinute = EEPROM.read(EEPROM_ADDR_OFF_MINUTE);
|
||||
} else resetToDefaultSchedule();
|
||||
EEPROM.end();
|
||||
}
|
||||
|
||||
void resetToDefaultSchedule() {
|
||||
lampuAutoMode = true;
|
||||
lampuOnHour = 6; lampuOnMinute = 0;
|
||||
lampuOffHour = 18; lampuOffMinute = 0;
|
||||
saveToEEPROM();
|
||||
}
|
||||
|
||||
// ========== WIFI & NTP ==========
|
||||
void setupWiFiManager() {
|
||||
WiFiManager wm;
|
||||
wm.setConfigPortalTimeout(180);
|
||||
if (!wm.autoConnect("ESP32_Aquamonn", "12345678")) {
|
||||
Serial.println("❌ Gagal konek WiFi! Restart...");
|
||||
delay(5000);
|
||||
ESP.restart();
|
||||
}
|
||||
}
|
||||
|
||||
bool isTimeValid() {
|
||||
struct tm timeinfo;
|
||||
if (!getLocalTime(&timeinfo, 1000)) return false;
|
||||
return (timeinfo.tm_year + 1900) >= 2024;
|
||||
}
|
||||
|
||||
void initNTP() {
|
||||
Serial.print("🕐 NTP Syncing...");
|
||||
configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);
|
||||
int attempts = 0;
|
||||
while (!isTimeValid() && attempts++ < 30) {
|
||||
Serial.print(".");
|
||||
delay(1000);
|
||||
}
|
||||
ntpSynced = isTimeValid();
|
||||
Serial.println(ntpSynced ? " ✅" : " ⚠️ Failed");
|
||||
if (ntpSynced) printWaktuSekarang();
|
||||
}
|
||||
|
||||
void syncNTP() {
|
||||
if (millis() - lastNTPSync > INTERVAL_NTP_SYNC) {
|
||||
lastNTPSync = millis();
|
||||
if (!isTimeValid()) {
|
||||
configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);
|
||||
delay(2000);
|
||||
ntpSynced = isTimeValid();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String getWaktuString() {
|
||||
struct tm timeinfo;
|
||||
if (!getLocalTime(&timeinfo) || !isTimeValid()) return "00:00:00";
|
||||
char buf[20];
|
||||
sprintf(buf, "%02d:%02d:%02d", timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec);
|
||||
return String(buf);
|
||||
}
|
||||
|
||||
String getTanggalString() {
|
||||
struct tm timeinfo;
|
||||
if (!getLocalTime(&timeinfo) || !isTimeValid()) return "0000-00-00";
|
||||
char buf[20];
|
||||
sprintf(buf, "%04d-%02d-%02d", timeinfo.tm_year + 1900, timeinfo.tm_mon + 1, timeinfo.tm_mday);
|
||||
return String(buf);
|
||||
}
|
||||
|
||||
int getCurrentHour() {
|
||||
struct tm timeinfo;
|
||||
if (!getLocalTime(&timeinfo) || !isTimeValid()) return 0;
|
||||
return timeinfo.tm_hour;
|
||||
}
|
||||
|
||||
int getCurrentMinute() {
|
||||
struct tm timeinfo;
|
||||
if (!getLocalTime(&timeinfo) || !isTimeValid()) return 0;
|
||||
return timeinfo.tm_min;
|
||||
}
|
||||
|
||||
void printWaktuSekarang() {
|
||||
struct tm timeinfo;
|
||||
if (getLocalTime(&timeinfo) && isTimeValid()) {
|
||||
Serial.printf(" 📅 %02d:%02d:%02d - %04d-%02d-%02d\n",
|
||||
timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec,
|
||||
timeinfo.tm_year + 1900, timeinfo.tm_mon + 1, timeinfo.tm_mday);
|
||||
}
|
||||
}
|
||||
|
||||
// ========== MQTT ==========
|
||||
void connectMQTT() {
|
||||
while (!client.connected()) {
|
||||
String clientId = "ESP32_Aquamonn_" + String(WiFi.macAddress());
|
||||
clientId.replace(":", "");
|
||||
Serial.printf("🔌 MQTT connecting...\n");
|
||||
if (client.connect(clientId.c_str())) {
|
||||
Serial.println("✅ MQTT Terhubung!");
|
||||
client.subscribe(topic_relay);
|
||||
client.subscribe(topic_lampu);
|
||||
client.subscribe(topic_schedule);
|
||||
kirimStatusPompa();
|
||||
kirimStatusLampu();
|
||||
kirimData();
|
||||
} else {
|
||||
Serial.printf("❌ Gagal, state=%d, coba lagi 5dtk\n", client.state());
|
||||
delay(5000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ========== FUNGSI UTAMA ==========
|
||||
void kontrolLampuBySchedule() {
|
||||
if (!lampuAutoMode || !isTimeValid()) return;
|
||||
int currentTotalMinutes = getCurrentHour() * 60 + getCurrentMinute();
|
||||
int onTotalMinutes = lampuOnHour * 60 + lampuOnMinute;
|
||||
int offTotalMinutes = lampuOffHour * 60 + lampuOffMinute;
|
||||
|
||||
bool seharusnyaNyala = (onTotalMinutes < offTotalMinutes) ?
|
||||
(currentTotalMinutes >= onTotalMinutes && currentTotalMinutes < offTotalMinutes) :
|
||||
(currentTotalMinutes >= onTotalMinutes || currentTotalMinutes < offTotalMinutes);
|
||||
|
||||
if (seharusnyaNyala != lampuAktif) {
|
||||
digitalWrite(RELAY_LAMPU, seharusnyaNyala ? LOW : HIGH);
|
||||
lampuAktif = seharusnyaNyala;
|
||||
Serial.printf("💡 Lampu: %s (schedule)\n", lampuAktif ? "ON" : "OFF");
|
||||
kirimStatusLampu();
|
||||
}
|
||||
}
|
||||
|
||||
void setLampu(String command) {
|
||||
if (command == "ON" || command == "1") {
|
||||
digitalWrite(RELAY_LAMPU, LOW);
|
||||
lampuAktif = true;
|
||||
lampuAutoMode = false;
|
||||
} else if (command == "OFF" || command == "0") {
|
||||
digitalWrite(RELAY_LAMPU, HIGH);
|
||||
lampuAktif = false;
|
||||
lampuAutoMode = false;
|
||||
}
|
||||
saveToEEPROM();
|
||||
kirimStatusLampu();
|
||||
Serial.printf("💡 Lampu: %s (manual)\n", lampuAktif ? "ON" : "OFF");
|
||||
}
|
||||
|
||||
void setLampuSchedule(String payload) {
|
||||
StaticJsonDocument<200> doc;
|
||||
if (deserializeJson(doc, payload)) {
|
||||
Serial.println("❌ JSON parse error");
|
||||
return;
|
||||
}
|
||||
|
||||
bool needSave = false;
|
||||
if (doc.containsKey("auto_mode")) {
|
||||
lampuAutoMode = doc["auto_mode"];
|
||||
needSave = true;
|
||||
}
|
||||
|
||||
if (lampuAutoMode) {
|
||||
if (doc.containsKey("on_hour")) { lampuOnHour = doc["on_hour"]; needSave = true; }
|
||||
if (doc.containsKey("on_minute")) { lampuOnMinute = doc["on_minute"]; needSave = true; }
|
||||
if (doc.containsKey("off_hour")) { lampuOffHour = doc["off_hour"]; needSave = true; }
|
||||
if (doc.containsKey("off_minute")) { lampuOffMinute = doc["off_minute"]; needSave = true; }
|
||||
}
|
||||
|
||||
if (needSave) {
|
||||
saveToEEPROM();
|
||||
scheduleChanged = true;
|
||||
lastScheduleChange = millis();
|
||||
kontrolLampuBySchedule();
|
||||
Serial.printf("📅 Jadwal: %02d:%02d - %02d:%02d\n", lampuOnHour, lampuOnMinute, lampuOffHour, lampuOffMinute);
|
||||
}
|
||||
kirimStatusLampu();
|
||||
}
|
||||
|
||||
void kontrolPompa() {
|
||||
if (!pompaAutoMode) return;
|
||||
bool seharusnyaNyala = (amonia > batasAmonia);
|
||||
if (seharusnyaNyala != pompaAktif) {
|
||||
digitalWrite(RELAY_POMPA, seharusnyaNyala ? LOW : HIGH);
|
||||
pompaAktif = seharusnyaNyala;
|
||||
Serial.printf("💧 Pompa: %s (NH₃: %.2f%%)\n", pompaAktif ? "ON" : "OFF", amonia);
|
||||
kirimStatusPompa();
|
||||
}
|
||||
}
|
||||
|
||||
void setPompaConfig(String payload) {
|
||||
StaticJsonDocument<200> doc;
|
||||
if (deserializeJson(doc, payload)) return;
|
||||
|
||||
if (doc.containsKey("pompa_auto_mode")) pompaAutoMode = doc["pompa_auto_mode"];
|
||||
if (doc.containsKey("batas_amonia")) batasAmonia = doc["batas_amonia"];
|
||||
|
||||
if (!pompaAutoMode && doc.containsKey("pompa_command")) {
|
||||
String cmd = doc["pompa_command"];
|
||||
digitalWrite(RELAY_POMPA, cmd == "ON" ? LOW : HIGH);
|
||||
pompaAktif = (cmd == "ON");
|
||||
kirimStatusPompa();
|
||||
}
|
||||
}
|
||||
|
||||
void kirimStatusLampu() {
|
||||
StaticJsonDocument<200> doc;
|
||||
doc["lampu_status"] = lampuAktif ? "ON" : "OFF";
|
||||
doc["lampu_motime"] = String(lampuOnHour) + ":" + String(lampuOnMinute);
|
||||
doc["lampu_off_time"] = String(lampuOffHour) + ":" + String(lampuOffMinute);
|
||||
doc["ntp_synced"] = ntpSynced && isTimeValid();
|
||||
char buffer[200];
|
||||
serializeJson(doc, buffer);
|
||||
client.publish(topic_lampu_status, buffer);
|
||||
}
|
||||
|
||||
void kirimStatusPompa() {
|
||||
StaticJsonDocument<150> doc;
|
||||
doc["relay"] = pompaAktif ? 1 : 0;
|
||||
doc["relay_status"] = pompaAktif ? "ON" : "OFF";
|
||||
doc["pompa_mode"] = pompaAutoMode ? "AUTO" : "MANUAL";
|
||||
doc["batas_amonia"] = batasAmonia;
|
||||
char buffer[150];
|
||||
serializeJson(doc, buffer);
|
||||
client.publish(topic_relay_status, buffer);
|
||||
}
|
||||
|
||||
// ========== SENSOR ==========
|
||||
void bacaSensor() {
|
||||
// Baca suhu
|
||||
sensors.requestTemperatures();
|
||||
float temp = sensors.getTempCByIndex(0);
|
||||
suhu = (temp != DEVICE_DISCONNECTED_C && !isnan(temp)) ? temp : 25.0;
|
||||
|
||||
// Baca pH
|
||||
int raw = analogRead(PH_SENSOR_PIN);
|
||||
voltPh = raw * (3.3 / 4095.0);
|
||||
pH = (voltPh >= V7) ? 7.0 + ((voltPh - V7) / (V7 - V0)) * 3.0 : 7.0 + ((voltPh - V7) / (V10 - V7)) * 3.0;
|
||||
pH = constrain(pH, 0, 14);
|
||||
|
||||
amonia = constrain((pH < 6.5) ? (pH / suhu) * 0.202 :
|
||||
(pH >= 6.5 && pH <= 7.0) ? (pH / suhu) * 1.131 : (pH / suhu) * 3.306, 0, 10);
|
||||
}
|
||||
|
||||
void kirimData() {
|
||||
if (!client.connected()) return;
|
||||
|
||||
StaticJsonDocument<350> doc;
|
||||
doc["suhu"] = suhu;
|
||||
doc["pH"] = pH;
|
||||
doc["amonia"] = amonia; // Persentase NH₃ (%)
|
||||
doc["volt_ph"] = voltPh;
|
||||
doc["relay"] = pompaAktif ? 1 : 0;
|
||||
doc["lampu"] = lampuAktif ? 1 : 0;
|
||||
doc["batas_amonia"] = batasAmonia;
|
||||
doc["lampu_mode"] = lampuAutoMode ? "AUTO" : "MANUAL";
|
||||
doc["lampu_on_time"] = String(lampuOnHour) + ":" + String(lampuOnMinute);
|
||||
doc["lampu_off_time"] = String(lampuOffHour) + ":" + String(lampuOffMinute);
|
||||
doc["jam"] = getWaktuString();
|
||||
doc["tanggal"] = getTanggalString();
|
||||
doc["ntp_synced"] = ntpSynced && isTimeValid();
|
||||
|
||||
char buffer[400];
|
||||
serializeJson(doc, buffer);
|
||||
|
||||
if (client.publish(topic_data, buffer)) {
|
||||
Serial.println("\n─────────────────────────────────────────");
|
||||
Serial.printf("📅 %s\n", getWaktuString().c_str());
|
||||
Serial.printf("🌡️ %.1f°C | 🔬 %.2f | 💊 NH₃: %.2f mg/L\n", suhu, pH, amonia);
|
||||
Serial.printf("💧 %s | 💡 %s (%s)\n", pompaAktif ? "ON" : "OFF", lampuAktif ? "ON" : "OFF", lampuAutoMode ? "AUTO" : "MANUAL");
|
||||
Serial.printf("📅 Jadwal: %02d:%02d - %02d:%02d\n", lampuOnHour, lampuOnMinute, lampuOffHour, lampuOffMinute);
|
||||
Serial.println("─────────────────────────────────────────");
|
||||
} else {
|
||||
Serial.println("❌ Gagal publish data!");
|
||||
}
|
||||
}
|
||||
|
||||
// ========== MQTT CALLBACK ==========
|
||||
void callback(char* topic, byte* payload, unsigned int length) {
|
||||
payload[length] = '\0';
|
||||
String message = String((char*)payload);
|
||||
Serial.printf("📨 %s: %s\n", topic, message.c_str());
|
||||
|
||||
if (strcmp(topic, topic_relay) == 0) setPompaConfig(message);
|
||||
else if (strcmp(topic, topic_lampu) == 0) setLampu(message);
|
||||
else if (strcmp(topic, topic_schedule) == 0) setLampuSchedule(message);
|
||||
}
|
||||
|
||||
// ========== SETUP ==========
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
delay(1000);
|
||||
|
||||
loadFromEEPROM();
|
||||
pinMode(RELAY_LAMPU, OUTPUT);
|
||||
pinMode(RELAY_POMPA, OUTPUT);
|
||||
digitalWrite(RELAY_LAMPU, HIGH);
|
||||
digitalWrite(RELAY_POMPA, HIGH);
|
||||
sensors.begin();
|
||||
pinMode(PH_SENSOR_PIN, INPUT);
|
||||
|
||||
setupWiFiManager();
|
||||
initNTP();
|
||||
client.setServer(mqtt_server, mqtt_port);
|
||||
client.setCallback(callback);
|
||||
connectMQTT();
|
||||
Serial.println("✅ Sistem siap!\n");
|
||||
printWaktuSekarang();
|
||||
}
|
||||
|
||||
// ========== LOOP ==========
|
||||
void loop() {
|
||||
if (!client.connected()) connectMQTT();
|
||||
client.loop();
|
||||
syncNTP();
|
||||
|
||||
unsigned long now = millis();
|
||||
if (now - lastSensorRead >= INTERVAL_SENSOR) {
|
||||
lastSensorRead = now;
|
||||
bacaSensor();
|
||||
kontrolPompa();
|
||||
kontrolLampuBySchedule();
|
||||
}
|
||||
|
||||
if (now - lastMQTT >= INTERVAL_SENSOR) {
|
||||
lastMQTT = now;
|
||||
kirimData();
|
||||
}
|
||||
|
||||
if (scheduleChanged && (now - lastScheduleChange > 2000)) {
|
||||
scheduleChanged = false;
|
||||
Serial.println("✅ Jadwal baru aktif!");
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue