1397 lines
48 KiB
C++
1397 lines
48 KiB
C++
#include <DHT.h>
|
|
#include <ESP32Servo.h>
|
|
#include <Wire.h>
|
|
#include <AS5600.h>
|
|
#include <WiFi.h>
|
|
#include <HTTPClient.h>
|
|
#include <SPIFFS.h>
|
|
#include <AsyncTCP.h>
|
|
#include <ESPAsyncWebServer.h>
|
|
#include <RTClib.h>
|
|
#include <EEPROM.h>
|
|
#include <ArduinoJson.h>
|
|
#include <LiquidCrystal_I2C.h> // WARNING: Library ini mungkin tidak sepenuhnya kompatibel dengan ESP32, namun seringkali berfungsi.
|
|
|
|
// --- PIN DEFINITIONS ---
|
|
#define DHTPIN 4
|
|
#define DHTTYPE DHT22
|
|
#define HEATER_PIN 19
|
|
#define FAN_PIN 18
|
|
#define FAN_PIN_2 26
|
|
#define MISTMAKER_PIN 12
|
|
#define SERVO_PIN 14
|
|
#define LIMIT_SWITCH_PIN 27
|
|
#define TRIAC_PIN 13
|
|
|
|
#define HEATER_PWM_CHANNEL 10
|
|
#define PWM_FREQUENCY 510
|
|
#define PWM_RESOLUTION 8
|
|
#define MAX_PWM_DUTY ((1 << PWM_RESOLUTION) - 1)
|
|
|
|
#define I2C_SDA_PIN_0 21
|
|
#define I2C_SCL_PIN_0 22
|
|
|
|
#define I2C_SDA_PIN_1 32
|
|
#define I2C_SCL_PIN_1 33
|
|
|
|
|
|
// --- EEPROM ADDRESSES ---
|
|
#define EEPROM_ADDR_DATE 0 // 3 bytes for Day, Month, Year
|
|
#define EEPROM_ADDR_INCUBATION_STARTED 3 // 1 byte for boolean status (0 or 1)
|
|
#define EEPROM_ADDR_CENTER_RAW 4 // 2 bytes for int (AS5600 center raw value)
|
|
#define EEPROM_ADDR_LAST_DAY 6 // 1 byte for last saved currentDay (max 21)
|
|
|
|
|
|
#define EEPROM_ADDR_TEMP_HISTORY (EEPROM_ADDR_LAST_DAY + 1) // Mulai setelah lastSavedDay
|
|
#define EEPROM_ADDR_HUM_HISTORY (EEPROM_ADDR_TEMP_HISTORY + (sizeof(float) * 21))
|
|
#define EEPROM_ADDR_RACK_ANGLE_HISTORY (EEPROM_ADDR_HUM_HISTORY + (sizeof(float) * 21))
|
|
#define EEPROM_ADDR_VENT_ANGLE_HISTORY (EEPROM_ADDR_RACK_ANGLE_HISTORY + (sizeof(int) * 21))
|
|
|
|
|
|
// Tambahkan definisi EEPROM_SIZE ini di bagian atas file Anda
|
|
const int EEPROM_SIZE = EEPROM_ADDR_VENT_ANGLE_HISTORY + (sizeof(int) * 21);
|
|
|
|
// Total EEPROM size needed: Alamat tertinggi (6) + ukuran variabel (1 byte) = 7 byte.
|
|
// EEPROM.begin() akan disesuaikan secara otomatis saat kompilasi berdasarkan alamat tertinggi + 1.
|
|
|
|
// --- GLOBAL OBJECTS ---
|
|
DHT dht(DHTPIN, DHTTYPE);
|
|
Servo myServo;
|
|
AS5600 encoder(&Wire1); // AS5600 menggunakan Wire1 (I2C Bus 1)
|
|
RTC_DS3231 rtc; // RTC menggunakan Wire (I2C Bus 0)
|
|
AsyncWebServer server(80);
|
|
LiquidCrystal_I2C lcd(0x27, 16, 2); // LCD menggunakan Wire (I2C Bus 0)
|
|
|
|
// --- WIFI CREDENTIALS ---
|
|
const char* ssid = "MINDIL";
|
|
const char* password = "mewah12345";
|
|
|
|
// --- GLOBAL VARIABLES ---
|
|
float suhu = NAN;
|
|
float kelembapan = NAN;
|
|
bool heaterState = false;
|
|
bool mistState = false;
|
|
bool isAutoMode = false;
|
|
bool isHeaterOn = false; // Status heater ON/OFF
|
|
bool isMistMakerOn = false; // Status mist maker ON/OFF
|
|
bool isIncubationStarted = false; // Variabel global untuk melacak status inkubasi
|
|
bool isRackMotorMoving = false;
|
|
int currentDay = 1; // Hari inkubasi saat ini
|
|
int lastSavedDay = 1; // Hari inkubasi terakhir yang disimpan di EEPROM
|
|
bool isWiFiConnected = false;
|
|
bool webServerStarted = false;
|
|
|
|
bool manualRackRotationActive = false; // Status apakah motor sedang berputar karena perintah manual
|
|
unsigned long lastManualRotateMillis = 0; // Waktu terakhir putar manual dimulai
|
|
const unsigned long MANUAL_ROTATE_DURATION = 7000; // Durasi putar manual dalam ms (sesuaikan dengan RotateByTimer)
|
|
|
|
|
|
int currentServoPos = 40;
|
|
int servoAngleByDay = 40;
|
|
|
|
unsigned long lastDHTRead = 0;
|
|
const unsigned long DHT_INTERVAL = 10000; // 10 detik
|
|
|
|
int targetPwmDuty = MAX_PWM_DUTY;
|
|
int currentPwmDuty = MAX_PWM_DUTY;
|
|
const int PWM_STEP = 10; // Langkah perubahan PWM
|
|
unsigned long lastPwmUpdateTime = 0;
|
|
const unsigned long PWM_UPDATE_INTERVAL = 235; // Update PWM setiap 20ms untuk transisi halus
|
|
|
|
DateTime tanggalMulai;
|
|
int overrideDay = 0; // Digunakan untuk override hari inkubasi secara manual
|
|
|
|
unsigned long lastStatusPrint = 0;
|
|
const unsigned long STATUS_PRINT_INTERVAL = 2000; // Print status ke Serial setiap 2 detik
|
|
|
|
const int NUM_DHT_READINGS = 10; // Jumlah pembacaan untuk filter DHT
|
|
float dhtTempReadings[NUM_DHT_READINGS];
|
|
float dhtHumReadings[NUM_DHT_READINGS];
|
|
int dhtReadingIndex = 0;
|
|
bool dhtReadingsInitialized = false;
|
|
|
|
//int rawDiffReadings[NUM_ANGLE_READINGS];
|
|
int angleReadingIndex = 0;
|
|
bool angleReadingsInitialized = false;
|
|
|
|
unsigned long lastWiFiReconnectAttempt = 0;
|
|
const unsigned long WIFI_RECONNECT_INTERVAL = 30000; // Coba reconnect WiFi setiap 30 detik
|
|
|
|
unsigned long lastLcdUpdate = 0;
|
|
const unsigned long LCD_UPDATE_INTERVAL = 1000; // Update LCD setiap 5 detik
|
|
|
|
// --- NEW GLOBAL ARRAYS FOR DAILY HISTORY ---
|
|
float dailyTemperatures[21]; // Array untuk menyimpan suhu harian
|
|
float dailyHumidities[21]; // Array untuk menyimpan kelembaban harian
|
|
|
|
int dailyVentAngles[21]; // Array untuk menyimpan sudut ventilasi harian
|
|
|
|
// --- FUNCTION DECLARATIONS ---
|
|
void matikanSemua();
|
|
void tampilkanStatus();
|
|
bool bolehGerakKe(int targetAngle);
|
|
void connectToWiFi();
|
|
|
|
|
|
|
|
float readFilteredTemperature();
|
|
float readFilteredHumidity();
|
|
|
|
void simpanStatusInkubasi(bool started);
|
|
bool bacaStatusInkubasi();
|
|
void simpanTanggalMulai(DateTime dt);
|
|
DateTime bacaTanggalMulai();
|
|
void simpanHariTerakhir(int day);
|
|
int bacaHariTerakhir();
|
|
void hitungHariInkubasi(); // Logika di dalamnya akan disesuaikan
|
|
void setTanggalMulaiManualHariKe(int hariKe);
|
|
void scanI2CBus(TwoWire *wireObj, const char* busName, int sdaPin, int sclPin);
|
|
|
|
// NEW FUNCTION DECLARATIONS
|
|
void saveDailyHistory(int dayIndex, float temp, float hum, int ventAngle);
|
|
void loadDailyHistory();
|
|
void clearDailyHistory(); // Fungsi untuk mereset data historis (saat inkubasi berhenti)
|
|
|
|
|
|
//UNTUK RAK TELUR
|
|
#define HALL_DO_PIN 23
|
|
#define TRIAC_PIN 13
|
|
bool motorNyala = false;
|
|
unsigned long lastStartTime = 0;
|
|
#define ROT_NONE 0
|
|
#define ROT_ZERO 1
|
|
#define ROT_SKEW 2
|
|
static volatile int rotateState = ROT_NONE;
|
|
static volatile int rotateStateTarget = ROT_NONE;
|
|
int sudutZero = 0;
|
|
float sudutZeroDerajat = 0;
|
|
float sudutSekarang = 0;
|
|
float sudutSekarangDerajat = 0;
|
|
bool rotateFindZero = false;
|
|
uint32_t tRotate = 0;
|
|
uint32_t jedaPutarRak = 14400000;//5000;//14400000; //14400000 = 4 jam
|
|
|
|
bool putarRakByTimer = true; //kalau false coba pakai sensor sudut. kalau true, dibatasi sama sudut dan timer
|
|
uint32_t tStopPutar = 0;
|
|
|
|
uint32_t lastTickUpdate = 0;
|
|
|
|
bool detachServoFlag = false;
|
|
uint32_t detachServoTick = 0;
|
|
uint32_t skipTick = 0;
|
|
|
|
// --- FUNCTION IMPLEMENTATIONS ---
|
|
|
|
void scanI2CBus(TwoWire *wireObj, const char* busName, int sdaPin, int sclPin) {
|
|
byte error, address;
|
|
int nDevices = 0;
|
|
Serial.printf("\n--- Scanning I2C Bus %s (SDA=%d, SCL=%d) ---\n", busName, sdaPin, sclPin);
|
|
Serial.println(" 0 1 2 3 4 5 6 7 8 9 A B C D E F");
|
|
|
|
for (int i = 0; i < 128; i += 16) {
|
|
Serial.printf("%02x ", i);
|
|
for (address = 0; address < 16; address++) {
|
|
if (i + address < 8) { // Alamat 0-7 dicadangkan, tidak perlu scan
|
|
Serial.print(" ");
|
|
continue;
|
|
}
|
|
wireObj->beginTransmission(i + address);
|
|
error = wireObj->endTransmission();
|
|
|
|
if (error == 0) {
|
|
Serial.printf("%02x ", i + address);
|
|
nDevices++;
|
|
} else if (error == 4) {
|
|
Serial.print("?? "); // Unknown error
|
|
} else {
|
|
Serial.print("-- "); // No device found
|
|
}
|
|
}
|
|
Serial.println();
|
|
}
|
|
|
|
if (nDevices == 0) {
|
|
Serial.printf("\nNo I2C devices found on Bus %s.\n", busName);
|
|
} else {
|
|
Serial.printf("\nDone scanning I2C Bus %s. Found %d device(s).\n", busName, nDevices);
|
|
}
|
|
Serial.println("------------------------------------------");
|
|
}
|
|
|
|
void simpanTanggalMulai(DateTime dt) {
|
|
EEPROM.write(EEPROM_ADDR_DATE, dt.day());
|
|
EEPROM.write(EEPROM_ADDR_DATE + 1, dt.month());
|
|
EEPROM.write(EEPROM_ADDR_DATE + 2, dt.year() - 2000);
|
|
EEPROM.commit();
|
|
Serial.printf("Tanggal mulai inkubasi disimpan: %02d-%02d-%04d\n", dt.day(), dt.month(), dt.year());
|
|
}
|
|
|
|
DateTime bacaTanggalMulai() {
|
|
int d = EEPROM.read(EEPROM_ADDR_DATE);
|
|
int m = EEPROM.read(EEPROM_ADDR_DATE + 1);
|
|
int y = EEPROM.read(EEPROM_ADDR_DATE + 2) + 2000;
|
|
// Cek validitas data dasar (contoh: tahun 2000-2099)
|
|
if (d < 1 || d > 31 || m < 1 || m > 12 || y < 2000 || y > 2099) {
|
|
Serial.println("Tanggal mulai di EEPROM tidak valid, menggunakan tanggal default.");
|
|
return DateTime(2025, 5, 20); // Contoh default tanggal
|
|
}
|
|
Serial.printf("Tanggal mulai dibaca dari EEPROM: %02d-%02d-%04d\n", d, m, y);
|
|
return DateTime(y, m, d);
|
|
}
|
|
|
|
void simpanStatusInkubasi(bool started) {
|
|
EEPROM.write(EEPROM_ADDR_INCUBATION_STARTED, started ? 1 : 0);
|
|
EEPROM.commit();
|
|
isIncubationStarted = started; // Pastikan variabel global juga terupdate
|
|
Serial.printf("Status inkubasi disimpan: %s\n", started ? "DIMULAI" : "BELUM DIMULAI");
|
|
}
|
|
|
|
bool bacaStatusInkubasi() {
|
|
byte status = EEPROM.read(EEPROM_ADDR_INCUBATION_STARTED);
|
|
// Jika nilai yang dibaca tidak valid (misal, EEPROM belum pernah ditulis), anggap belum dimulai
|
|
if (status != 0 && status != 1) {
|
|
Serial.println("Status inkubasi di EEPROM tidak valid, dianggap BELUM DIMULAI.");
|
|
return false;
|
|
}
|
|
isIncubationStarted = (status == 1); // Perbarui variabel global
|
|
Serial.printf("Status inkubasi dibaca dari EEPROM: %s\n", isIncubationStarted ? "DIMULAI" : "BELUM DIMULAI");
|
|
return isIncubationStarted;
|
|
}
|
|
|
|
void simpanHariTerakhir(int day) {
|
|
if (day < 1) day = 1; // Pastikan hari tidak kurang dari 1
|
|
if (day > 21) day = 21; // Pastikan hari tidak lebih dari 21
|
|
EEPROM.write(EEPROM_ADDR_LAST_DAY, day);
|
|
EEPROM.commit();
|
|
lastSavedDay = day; // Update variabel global
|
|
Serial.printf("Hari terakhir inkubasi disimpan ke EEPROM: %d\n", day);
|
|
}
|
|
|
|
int bacaHariTerakhir() {
|
|
int day = EEPROM.read(EEPROM_ADDR_LAST_DAY);
|
|
// Jika nilai tidak valid (misal, EEPROM belum pernah ditulis), anggap hari 1
|
|
if (day < 1 || day > 21) {
|
|
Serial.println("Hari terakhir di EEPROM tidak valid, dianggap hari ke-1.");
|
|
return 1;
|
|
}
|
|
lastSavedDay = day; // Update variabel global
|
|
Serial.printf("Hari terakhir dibaca dari EEPROM: %d\n", lastSavedDay);
|
|
return lastSavedDay;
|
|
}
|
|
|
|
void hitungHariInkubasi() {
|
|
// Fungsi ini sekarang lebih fokus pada perhitungan real-time untuk tujuan display/debug jika diperlukan,
|
|
// atau untuk set tanggalMulai saat start baru.
|
|
// currentDay aktual untuk logika utama akan diatur oleh lastSavedDay atau setTanggalMulaiManualHariKe.
|
|
if (overrideDay > 0) {
|
|
// Jika ada override, currentDay diset langsung di setTanggalMulaiManualHariKe
|
|
} else if (!isIncubationStarted) {
|
|
// Hanya hitung dari RTC jika inkubasi belum dimulai.
|
|
// Ini untuk setting awal tanggalMulai atau saat pertama kali siklus dijalankan.
|
|
DateTime now = rtc.now();
|
|
TimeSpan durasi = now - tanggalMulai;
|
|
currentDay = durasi.days() + 1;
|
|
if (currentDay < 1) currentDay = 1;
|
|
if (currentDay > 21) currentDay = 21;
|
|
}
|
|
// Jika isIncubationStarted TRUE dan tidak ada overrideDay, currentDay akan mempertahankan
|
|
// nilai dari lastSavedDay yang dimuat di setup() atau yang terakhir disimpan secara eksplisit.
|
|
// Logika untuk maju hari otomatis ada di loop().
|
|
}
|
|
|
|
void setTanggalMulaiManualHariKe(int hariKe) {
|
|
DateTime now = rtc.now();
|
|
if (hariKe < 1) hariKe = 1;
|
|
if (hariKe > 21) hariKe = 21;
|
|
// Sesuaikan tanggalMulai agar hari saat ini menjadi hariKe
|
|
DateTime newStart = now - TimeSpan(hariKe - 1, 0, 0, 0);
|
|
tanggalMulai = newStart;
|
|
simpanTanggalMulai(tanggalMulai);
|
|
|
|
overrideDay = 0; // Membatalkan override hari
|
|
currentDay = hariKe; // Set currentDay langsung ke hari yang diminta
|
|
simpanHariTerakhir(currentDay); // <--- SIMPAN HARI KE EEPROM
|
|
isAutoMode = true; // Langsung setel ke mode otomatis
|
|
simpanStatusInkubasi(true); // <--- TANDAI INKUBASI SUDAH DIMULAI
|
|
tampilkanStatus();
|
|
Serial.printf("Tanggal mulai diset ke %02d-%02d-%04d agar hari sekarang adalah hari ke-%d, mode AUTO aktif.\n",
|
|
tanggalMulai.day(), tanggalMulai.month(), tanggalMulai.year(), hariKe);
|
|
}
|
|
|
|
float readFilteredTemperature() {
|
|
float rawTemp = dht.readTemperature();
|
|
if (isnan(rawTemp)) {
|
|
if (dhtReadingsInitialized || dhtReadingIndex > 0) {
|
|
float sum = 0;
|
|
int count = 0;
|
|
if (dhtReadingsInitialized) {
|
|
for (int i = 0; i < NUM_DHT_READINGS; i++) {
|
|
sum += dhtTempReadings[i];
|
|
}
|
|
count = NUM_DHT_READINGS;
|
|
} else {
|
|
for (int i = 0; i < dhtReadingIndex; i++) {
|
|
sum += dhtTempReadings[i];
|
|
}
|
|
count = dhtReadingIndex;
|
|
}
|
|
if (count > 0) return sum / count;
|
|
}
|
|
return suhu; // Mengembalikan nilai suhu terakhir yang valid jika pembacaan gagal
|
|
}
|
|
|
|
dhtTempReadings[dhtReadingIndex] = rawTemp;
|
|
|
|
dhtReadingIndex = (dhtReadingIndex + 1) % NUM_DHT_READINGS;
|
|
|
|
if (dhtReadingIndex == 0) {
|
|
dhtReadingsInitialized = true;
|
|
}
|
|
|
|
float sum = 0;
|
|
int count = 0;
|
|
if (dhtReadingsInitialized) {
|
|
for (int i = 0; i < NUM_DHT_READINGS; i++) {
|
|
sum += dhtTempReadings[i];
|
|
}
|
|
count = NUM_DHT_READINGS;
|
|
} else {
|
|
for (int i = 0; i < dhtReadingIndex; i++) {
|
|
sum += dhtTempReadings[i];
|
|
}
|
|
count = dhtReadingIndex;
|
|
}
|
|
|
|
if (count == 0) return rawTemp; // Seharusnya tidak terjadi jika rawTemp valid
|
|
return sum / count;
|
|
}
|
|
|
|
float readFilteredHumidity() {
|
|
float rawHum = dht.readHumidity();
|
|
if (isnan(rawHum)) {
|
|
if (dhtReadingsInitialized || dhtReadingIndex > 0) {
|
|
float sum = 0;
|
|
int count = 0;
|
|
if (dhtReadingsInitialized) {
|
|
for (int i = 0; i < NUM_DHT_READINGS; i++) {
|
|
sum += dhtHumReadings[i];
|
|
}
|
|
count = NUM_DHT_READINGS;
|
|
} else {
|
|
for (int i = 0; i < dhtReadingIndex; i++) {
|
|
sum += dhtHumReadings[i];
|
|
}
|
|
count = dhtReadingIndex;
|
|
}
|
|
if (count > 0) return sum / count;
|
|
}
|
|
return kelembapan; // Mengembalikan nilai kelembapan terakhir yang valid jika pembacaan gagal
|
|
}
|
|
|
|
dhtHumReadings[dhtReadingIndex] = rawHum;
|
|
|
|
// dhtReadingIndex sudah diupdate di readFilteredTemperature, jadi ini akan digunakan untuk keduanya
|
|
// dhtReadingIndex = (dhtReadingIndex + 1) % NUM_DHT_READINGS; // Tidak perlu di sini lagi, karena sudah di readFilteredTemperature
|
|
|
|
float sum = 0;
|
|
int count = 0;
|
|
if (dhtReadingsInitialized) {
|
|
for (int i = 0; i < NUM_DHT_READINGS; i++) {
|
|
sum += dhtHumReadings[i];
|
|
}
|
|
count = NUM_DHT_READINGS;
|
|
} else {
|
|
for (int i = 0; i < dhtReadingIndex; i++) {
|
|
sum += dhtHumReadings[i];
|
|
}
|
|
count = dhtReadingIndex;
|
|
}
|
|
|
|
if (count == 0) return rawHum; // Seharusnya tidak terjadi jika rawHum valid
|
|
return sum / count;
|
|
}
|
|
|
|
void bacaSensor() {
|
|
suhu = readFilteredTemperature();
|
|
kelembapan = readFilteredHumidity();
|
|
Serial.printf("Pembacaan DHT (Filtered): Suhu=%.2f, Kelembapan=%.2f\n", suhu, kelembapan);
|
|
}
|
|
|
|
void updateHeaterPWM() {
|
|
unsigned long now = millis();
|
|
if (now - lastPwmUpdateTime >= PWM_UPDATE_INTERVAL) {
|
|
if (suhu < 37.3){
|
|
targetPwmDuty += PWM_STEP;
|
|
}else if (suhu > 37.7){
|
|
targetPwmDuty -= 1;
|
|
}
|
|
if (targetPwmDuty<0) targetPwmDuty = 0;
|
|
if (targetPwmDuty>MAX_PWM_DUTY) targetPwmDuty = MAX_PWM_DUTY;
|
|
if (isAutoMode){
|
|
currentPwmDuty = targetPwmDuty;
|
|
}else{
|
|
currentPwmDuty = 0;
|
|
}
|
|
ledcWrite(HEATER_PWM_CHANNEL, currentPwmDuty);
|
|
lastPwmUpdateTime = now;
|
|
}
|
|
}
|
|
|
|
void connectToWiFi() {
|
|
if (WiFi.status() == WL_CONNECTED) {
|
|
if (!isWiFiConnected) {
|
|
isWiFiConnected = true;
|
|
Serial.print("WiFi Terhubung! Alamat IP: ");
|
|
Serial.println(WiFi.localIP());
|
|
lcd.clear();
|
|
lcd.print("WiFi Connected!");
|
|
lcd.setCursor(0, 1);
|
|
lcd.print(WiFi.localIP());
|
|
//delay(2000);
|
|
//lcd.clear();
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (WiFi.status() != WL_CONNECTED) {
|
|
if (isWiFiConnected) {
|
|
isWiFiConnected = false;
|
|
Serial.println("WiFi Terputus. Mencoba menyambung kembali...");
|
|
lcd.clear();
|
|
lcd.print("WiFi Disconnected");
|
|
lcd.setCursor(0, 1);
|
|
lcd.print("Retrying...");
|
|
}
|
|
|
|
// Hindari mencoba menyambung terlalu sering jika sudah dalam proses
|
|
if (millis() - lastWiFiReconnectAttempt >= WIFI_RECONNECT_INTERVAL || lastWiFiReconnectAttempt == 0) {
|
|
WiFi.mode(WIFI_STA);
|
|
WiFi.begin(ssid, password);
|
|
Serial.println("Mencoba menyambungkan WiFi...");
|
|
lastWiFiReconnectAttempt = millis();
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- NEW FUNCTION IMPLEMENTATIONS FOR DAILY HISTORY ---
|
|
|
|
// Menyimpan satu titik data harian ke EEPROM
|
|
void saveDailyHistory(int dayIndex, float temp, float hum, int ventAngle) {
|
|
if (dayIndex >= 0 && dayIndex < 21) {
|
|
// Simpan ke array global (cache)
|
|
dailyTemperatures[dayIndex] = temp;
|
|
dailyHumidities[dayIndex] = hum;
|
|
dailyVentAngles[dayIndex] = ventAngle;
|
|
|
|
// Simpan ke EEPROM
|
|
EEPROM.put(EEPROM_ADDR_TEMP_HISTORY + (dayIndex * sizeof(float)), temp);
|
|
EEPROM.put(EEPROM_ADDR_HUM_HISTORY + (dayIndex * sizeof(float)), hum);
|
|
EEPROM.put(EEPROM_ADDR_VENT_ANGLE_HISTORY + (dayIndex * sizeof(int)), ventAngle);
|
|
EEPROM.commit();
|
|
Serial.printf("Data historis Hari %d disimpan: Suhu=%.2f, RH=%.2f, Rak=%d, Vent=%d\n",
|
|
dayIndex + 1, temp, hum, ventAngle);
|
|
} else {
|
|
Serial.println("Indeks hari untuk penyimpanan histori tidak valid.");
|
|
}
|
|
}
|
|
|
|
// Memuat semua data historis dari EEPROM ke array global
|
|
void loadDailyHistory() {
|
|
Serial.println("Memuat data historis harian dari EEPROM...");
|
|
for (int i = 0; i < 21; i++) {
|
|
EEPROM.get(EEPROM_ADDR_TEMP_HISTORY + (i * sizeof(float)), dailyTemperatures[i]);
|
|
EEPROM.get(EEPROM_ADDR_HUM_HISTORY + (i * sizeof(float)), dailyHumidities[i]);
|
|
EEPROM.get(EEPROM_ADDR_VENT_ANGLE_HISTORY + (i * sizeof(int)), dailyVentAngles[i]);
|
|
// Validasi data yang dibaca (misal, jika EEPROM kosong awalnya)
|
|
if (isnan(dailyTemperatures[i]) || dailyTemperatures[i] < 0 || dailyTemperatures[i] > 100) dailyTemperatures[i] = 0.0;
|
|
if (isnan(dailyHumidities[i]) || dailyHumidities[i] < 0 || dailyHumidities[i] > 100) dailyHumidities[i] = 0.0;
|
|
if (dailyVentAngles[i] < 0 || dailyVentAngles[i] > 180) dailyVentAngles[i] = 0;
|
|
}
|
|
Serial.println("Data historis harian selesai dimuat.");
|
|
}
|
|
|
|
// Mengosongkan semua data historis di EEPROM dan RAM
|
|
void clearDailyHistory() {
|
|
Serial.println("Mengosongkan data historis harian...");
|
|
for (int i = 0; i < 21; i++) {
|
|
dailyTemperatures[i] = 0.0;
|
|
dailyHumidities[i] = 0.0;
|
|
dailyVentAngles[i] = 0;
|
|
// Opsional: Langsung tulis 0 ke EEPROM untuk clear total
|
|
EEPROM.put(EEPROM_ADDR_TEMP_HISTORY + (i * sizeof(float)), 0.0f);
|
|
EEPROM.put(EEPROM_ADDR_HUM_HISTORY + (i * sizeof(float)), 0.0f);
|
|
EEPROM.put(EEPROM_ADDR_VENT_ANGLE_HISTORY + (i * sizeof(int)), 0);
|
|
}
|
|
EEPROM.commit(); // Pastikan perubahan disimpan
|
|
Serial.println("Data historis harian berhasil dikosongkan.");
|
|
}
|
|
|
|
void setup() {
|
|
delay(2000);
|
|
Serial.begin(115200);
|
|
// Ukuran EEPROM.begin() harus mencakup alamat tertinggi yang digunakan + ukuran variabelnya
|
|
//EEPROM.begin(EEPROM_ADDR_LAST_DAY + 1); // Cukup 1 byte untuk hari (1-21)
|
|
EEPROM.begin(EEPROM_SIZE);
|
|
loadDailyHistory();
|
|
|
|
if (!SPIFFS.begin(true)) {
|
|
Serial.println("Gagal mount SPIFFS!");
|
|
while (true) delay(1000);
|
|
}
|
|
Serial.println("SPIFFS mounted successfully.");
|
|
|
|
Wire.begin(I2C_SDA_PIN_0, I2C_SCL_PIN_0);
|
|
Serial.printf("I2C Bus 0 (RTC & LCD) diinisialisasi: SDA=%d, SCL=%d\n", I2C_SDA_PIN_0, I2C_SCL_PIN_0);
|
|
|
|
lcd.init();
|
|
lcd.backlight();
|
|
lcd.print("Inkubator Nyala");
|
|
lcd.setCursor(0, 1);
|
|
lcd.print("Loading...");
|
|
delay(2000);
|
|
|
|
Wire1.begin(I2C_SDA_PIN_1, I2C_SCL_PIN_1);
|
|
Serial.printf("I2C Bus 1 (AS5600) diinisialisasi: SDA=%d, SCL=%d\n", I2C_SDA_PIN_1, I2C_SCL_PIN_1);
|
|
|
|
angleReadingsInitialized = true;
|
|
angleReadingIndex = 0;
|
|
|
|
dht.begin();
|
|
Serial.println("DHT22 sensor diinisialisasi.");
|
|
|
|
ledcSetup(HEATER_PWM_CHANNEL, PWM_FREQUENCY, PWM_RESOLUTION);
|
|
ledcAttachPin(HEATER_PIN, HEATER_PWM_CHANNEL);
|
|
ledcWrite(HEATER_PWM_CHANNEL, 0);
|
|
|
|
pinMode(FAN_PIN, OUTPUT);
|
|
pinMode(FAN_PIN_2, OUTPUT);
|
|
pinMode(MISTMAKER_PIN, OUTPUT);
|
|
pinMode(TRIAC_PIN, OUTPUT);
|
|
pinMode(LIMIT_SWITCH_PIN, INPUT_PULLUP);
|
|
|
|
digitalWrite(FAN_PIN, LOW);
|
|
digitalWrite(FAN_PIN_2, LOW);
|
|
digitalWrite(MISTMAKER_PIN, LOW);
|
|
digitalWrite(TRIAC_PIN, LOW);
|
|
|
|
myServo.attach(SERVO_PIN);
|
|
myServo.write(currentServoPos);
|
|
|
|
// --- PEMBACAAN DAN LOGIKA INISIALISASI RTC & EEPROM ---
|
|
if (!rtc.begin(&Wire)) {
|
|
Serial.println("RTC tidak terdeteksi atau gagal diinisialisasi!");
|
|
Serial.println("Periksa koneksi RTC (SDA=21, SCL=22) atau batere.");
|
|
// Jika RTC tidak ada, kita *tidak akan* mereset hari inkubasi ke 1 secara paksa,
|
|
// melainkan mengandalkan `lastSavedDay` jika inkubasi sudah dimulai.
|
|
// `tanggalMulai` akan diset ke waktu kompilasi sebagai fallback minimal.
|
|
tanggalMulai = DateTime(F(__DATE__), F(__TIME__));
|
|
simpanTanggalMulai(tanggalMulai); // Simpan tanggal kompilasi ini ke EEPROM
|
|
bacaStatusInkubasi(); // Baca status inkubasi dari EEPROM
|
|
bacaHariTerakhir(); // Baca hari terakhir dari EEPROM
|
|
// currentDay akan diatur berdasarkan lastSavedDay di bagian bawah setup()
|
|
Serial.println("RTC tidak aktif, mengandalkan data EEPROM dan waktu kompilasi sebagai fallback.");
|
|
} else {
|
|
Serial.println("RTC berhasil inisialisasi.");
|
|
bacaStatusInkubasi(); // Baca status inkubasi terlebih dahulu
|
|
bacaHariTerakhir(); // Baca hari terakhir terlebih dahulu
|
|
|
|
if (rtc.lostPower()) {
|
|
Serial.println("RTC kehilangan daya, waktu perlu disetel ulang!");
|
|
rtc.adjust(DateTime(F(__DATE__), F(__TIME__))); // Setel RTC ke waktu kompilasi
|
|
Serial.println("RTC telah disetel ulang ke waktu kompilasi.");
|
|
// PENTING: Kita TIDAK mereset isIncubationStarted atau currentDay di sini
|
|
// agar bisa melanjutkan dari data EEPROM jika inkubasi sudah berjalan.
|
|
// tanggalMulai akan disesuaikan di bawah jika 'now < tanggalMulai'
|
|
}
|
|
|
|
// Baca tanggalMulai dari EEPROM. Ini adalah tanggal yang kita simpan saat siklus dimulai.
|
|
tanggalMulai = bacaTanggalMulai();
|
|
}
|
|
|
|
DateTime now = rtc.now();
|
|
if (now < tanggalMulai) {
|
|
Serial.println("Peringatan: Waktu RTC saat ini lebih awal dari tanggal mulai yang dibaca dari EEPROM.");
|
|
Serial.println("Ini mungkin indikasi RTC disetel mundur atau masalah sinkronisasi.");
|
|
// PENTING: Kita TIDAK mereset isIncubationStarted atau currentDay di sini.
|
|
// Hanya sesuaikan tanggalMulai agar tidak ada error perhitungan durasi negatif.
|
|
tanggalMulai = now;
|
|
simpanTanggalMulai(tanggalMulai); // Simpan tanggalMulai yang baru (now)
|
|
// currentDay akan diatur berdasarkan lastSavedDay di bagian bawah setup()
|
|
}
|
|
|
|
// Penentuan mode awal saat boot
|
|
if (isIncubationStarted) {
|
|
// Jika inkubasi sudah dimulai (dari EEPROM), paksa ke mode otomatis
|
|
isAutoMode = true;
|
|
// Kita telah membaca lastSavedDay sebelumnya. Gunakan nilai itu!
|
|
currentDay = lastSavedDay;
|
|
Serial.printf("Sistem dinyalakan, inkubasi sudah berjalan. Mode OTOMATIS aktif, melanjutkan dari Hari ke-%d.\n", currentDay);
|
|
} else {
|
|
// Jika inkubasi belum dimulai, default ke mode manual
|
|
isAutoMode = false;
|
|
currentDay = 1; // Hari ke-1 jika siklus belum dimulai
|
|
simpanHariTerakhir(currentDay); // Pastikan hari ke-1 disimpan juga
|
|
Serial.println("Sistem dinyalakan, inkubasi belum dimulai. Mode MANUAL aktif secara default.");
|
|
}
|
|
// --- AKHIR LOGIKA INISIALISASI RTC & EEPROM ---
|
|
|
|
connectToWiFi(); // Coba koneksi WiFi
|
|
|
|
// --- WEB SERVER HANDLERS ---
|
|
server.serveStatic("/", SPIFFS, "/").setDefaultFile("login1.html");
|
|
|
|
server.on("/", HTTP_GET, [](AsyncWebServerRequest * request) {
|
|
request->redirect("/login1.html");
|
|
});
|
|
|
|
server.on("/data", HTTP_GET, [](AsyncWebServerRequest * request) {
|
|
bacaSensor();
|
|
// hitungHariInkubasi(); // Tidak perlu di sini, currentDay sudah diatur oleh logika di loop/setup
|
|
|
|
String json = "{";
|
|
json += "\"temp\":" + String(suhu, 2) + ",";
|
|
json += "\"hum\":" + String(kelembapan, 2) + ",";
|
|
json += "\"day\":" + String(currentDay) + ",";
|
|
json += "\"sudutVentilasi\":" + String(currentServoPos) + ",";
|
|
|
|
String rackAngleString = "";
|
|
|
|
if (posisiRakMiring()) {
|
|
rackAngleString = "Miring";
|
|
} else if (posisiRakTegak()) {
|
|
rackAngleString = "Tegak";
|
|
} else {
|
|
rackAngleString = "Belum Inisiasi";
|
|
}
|
|
json += "\"sudutRak\":\"" + rackAngleString + "\",";
|
|
|
|
int pwmDisplayValue = (currentPwmDuty*100) / MAX_PWM_DUTY;
|
|
|
|
json += "\"pwmHeater\":" + String(pwmDisplayValue) + ",";
|
|
json += "\"wifiStatus\":\"" + String(isWiFiConnected ? "CONNECTED" : "DISCONNECTED") + "\",";
|
|
json += "\"isAutoMode\":" + String(isAutoMode ? "true" : "false") + ","; // Tambahkan status mode
|
|
json += "\"isIncubationStarted\":" + String(isIncubationStarted ? "true" : "false"); // Tambahkan status siklus
|
|
|
|
json += ",\"isHeaterOn\":" + String(isHeaterOn ? "true" : "false");
|
|
json += ",\"isMistMakerOn\":" + String(isMistMakerOn ? "true" : "false");
|
|
json += ",\"isRackMotorMoving\":" + String(motorNyala ? "true" : "false");
|
|
|
|
json += ","; // Tambahkan koma sebelum menambahkan item baru
|
|
json += "\"currentRtcTime\":" + String(rtc.now().unixtime() * 1000);
|
|
json += ",\"startDate\":\""; // Tambahkan koma sebelum startDate
|
|
if (isIncubationStarted) {
|
|
char dateBuffer[11]; // YYYY-MM-DD\0
|
|
dateBuffer[10] = 0;
|
|
sprintf(dateBuffer, "%04d-%02d-%02d", tanggalMulai.year(), tanggalMulai.month(), tanggalMulai.day());
|
|
json += String(dateBuffer);
|
|
} else {
|
|
json += ""; // Kosong jika inkubasi belum dimulai
|
|
}
|
|
json += "\"";
|
|
json += "}";
|
|
|
|
request->send(200, "application/json", json);
|
|
});
|
|
|
|
|
|
server.on("/setday", HTTP_POST,
|
|
[](AsyncWebServerRequest * request) {
|
|
request->send(200, "application/json", "{\"message\":\"OK\"}");
|
|
},
|
|
NULL,
|
|
[](AsyncWebServerRequest * request, uint8_t *data, size_t len, size_t index, size_t total) {
|
|
static String body;
|
|
if (index == 0) body = "";
|
|
for (size_t i = 0; i < len; i++) {
|
|
body += (char)data[i];
|
|
}
|
|
if (index + len == total) {
|
|
DynamicJsonDocument doc(256);
|
|
auto error = deserializeJson(doc, body);
|
|
if (!error) {
|
|
int day = doc["day"];
|
|
if (day >= 1 && day <= 21) {
|
|
//set pwm ke max saat pertama kali aktif
|
|
targetPwmDuty = MAX_PWM_DUTY;
|
|
currentPwmDuty = MAX_PWM_DUTY;
|
|
overrideDay = day; // overrideDay akan digunakan oleh setTanggalMulaiManualHariKe
|
|
// Panggil fungsi setTanggalMulaiManualHariKe untuk mengatur hari dan menyimpannya
|
|
setTanggalMulaiManualHariKe(day); // Ini akan meng-update currentDay dan menyimpannya ke EEPROM
|
|
Serial.printf("Hari inkubasi di-set manual ke %d dari web, mode AUTO aktif\n", day);
|
|
} else {
|
|
Serial.println("Nilai hari inkubasi tidak valid dari web.");
|
|
}
|
|
} else {
|
|
Serial.println("JSON invalid diterima di /setday dari web");
|
|
Serial.println(error.c_str());
|
|
}
|
|
}
|
|
});
|
|
|
|
|
|
server.on("/stop", HTTP_POST, [](AsyncWebServerRequest * request) {
|
|
isAutoMode = false;
|
|
overrideDay = 0;
|
|
currentDay = 1; // Reset hari
|
|
tanggalMulai = rtc.now(); // Reset tanggal mulai ke sekarang
|
|
simpanTanggalMulai(tanggalMulai);
|
|
simpanStatusInkubasi(false);
|
|
isAutoMode = false;
|
|
Serial.println("Sistem diatur ke Mode MANUAL."); // Debug
|
|
simpanHariTerakhir(1);
|
|
clearDailyHistory(); // <--- PENTING: Kosongkan histori saat stop!
|
|
matikanSemua();
|
|
Serial.println("🛑 Inkubasi dihentikan manual dari dashboard");
|
|
request->send(200, "text/plain", "Inkubasi dihentikan");
|
|
});
|
|
|
|
server.on("/toggleRack", HTTP_POST, [](AsyncWebServerRequest * request) {
|
|
// Izinkan putaran rak manual HANYA jika inkubasi TIDAK berjalan
|
|
if (!isIncubationStarted) { // <--- KONDISI BARU: Cek isIncubationStarted
|
|
if (posisiRakMiring()) {
|
|
RotateZero();
|
|
request->send(200, "application/json", "{\"message\":\"Memulai putaran rak ke tegak.\"}");
|
|
} else if (posisiRakTegak()) {
|
|
RotateAgain();
|
|
request->send(200, "application/json", "{\"message\":\"Memulai putaran rak ke miring.\"}");
|
|
} else {
|
|
RotateZero();
|
|
request->send(200, "application/json", "{\"message\":\"Memulai putaran rak ke tegak.\"}");
|
|
}
|
|
} else { // Jika inkubasi sedang berjalan
|
|
Serial.println("🚫 Gerakan rak manual ditolak: Inkubasi sedang berjalan.");
|
|
request->send(403, "application/json", "{\"message\":\"Rak tidak dapat diputar secara manual saat inkubasi berjalan.\"}");
|
|
}
|
|
});
|
|
|
|
server.on("/setmode/auto", HTTP_POST, [](AsyncWebServerRequest * request) {
|
|
isAutoMode = true;
|
|
Serial.println("Mode: OTOMATIS aktif (via web).");
|
|
request->send(200, "text/plain", "Mode OTOMATIS aktif.");
|
|
});
|
|
|
|
server.on("/setmode/manual", HTTP_POST, [](AsyncWebServerRequest * request) {
|
|
if (!isIncubationStarted) { // Hanya izinkan manual jika inkubasi belum dimulai
|
|
isAutoMode = false;
|
|
|
|
Serial.println("Mode: MANUAL aktif (via web).");
|
|
request->send(200, "text/plain", "Mode MANUAL aktif.");
|
|
} else {
|
|
isAutoMode = true; // Paksa kembali ke otomatis jika sudah dimulai
|
|
Serial.println("Inkubasi sudah dimulai. Mode MANUAL tidak diizinkan (via web).");
|
|
request->send(403, "text/plain", "Mode MANUAL tidak diizinkan setelah inkubasi dimulai.");
|
|
}
|
|
});
|
|
|
|
server.onNotFound([](AsyncWebServerRequest * request) {
|
|
request->send(404, "text/plain", "File Not Found");
|
|
});
|
|
|
|
server.begin();
|
|
webServerStarted = true;
|
|
Serial.println("Web server dimulai.");
|
|
|
|
//buat rak telur
|
|
setupRakTelur();
|
|
//delay(2000);
|
|
|
|
}
|
|
|
|
void loop() {
|
|
unsigned long nowMillis = millis();
|
|
|
|
//limit stack call
|
|
if (nowMillis-skipTick>=50){
|
|
skipTick=nowMillis;
|
|
}else{
|
|
return;
|
|
}
|
|
|
|
if (Serial.available()) {
|
|
String input = Serial.readStringUntil('\n');
|
|
input.trim();
|
|
if (input.startsWith("start ")) {
|
|
int d, m, y;
|
|
if (sscanf(input.c_str(), "start %d-%d-%d", &d, &m, &y) == 3) {
|
|
tanggalMulai = DateTime(y, m, d);
|
|
simpanTanggalMulai(tanggalMulai);
|
|
|
|
currentDay = 1;
|
|
simpanHariTerakhir(currentDay);
|
|
|
|
isAutoMode = true;
|
|
overrideDay = 0;
|
|
simpanStatusInkubasi(true);
|
|
clearDailyHistory(); // <--- TAMBAHKAN BARIS INI
|
|
Serial.printf("Inkubasi dimulai dengan tanggal: %02d-%02d-%04d, hari ke-%d.\n", d, m, y, currentDay);
|
|
} else {
|
|
Serial.println("Format salah! Gunakan: start DD-MM-YYYY");
|
|
}
|
|
} else if (input.startsWith("setday ")) {
|
|
int h = input.substring(7).toInt();
|
|
setTanggalMulaiManualHariKe(h); // Fungsi ini sudah memanggil simpanStatusInkubasi(true) dan simpanHariTerakhir(currentDay)
|
|
} else if (input.equalsIgnoreCase("auto")) {
|
|
isAutoMode = true;
|
|
Serial.println("Mode: OTOMATIS aktif (termasuk motor).");
|
|
} else if (input.equalsIgnoreCase("manual")) {
|
|
if (!isIncubationStarted) { // Hanya izinkan manual jika inkubasi belum dimulai
|
|
isAutoMode = false;
|
|
Serial.println("Mode: MANUAL aktif (termasuk motor).");
|
|
|
|
} else {
|
|
Serial.println("Inkubasi sudah dimulai. Mode MANUAL tidak diizinkan.");
|
|
isAutoMode = true; // Paksa kembali ke otomatis jika sudah dimulai
|
|
}
|
|
} else if (input.equalsIgnoreCase("calibrate")) {
|
|
Serial.println("Perintah kalibrasi AS5600 manual tidak didukung lagi.");
|
|
Serial.println("Titik 0 derajat AS5600 otomatis disetel saat boot.");
|
|
}
|
|
}
|
|
|
|
////Serial.println("Debug point: 1");
|
|
|
|
// --- LOGIKA UNTUK MAJU HARI SECARA OTOMATIS SAAT INKUBASI BERJALAN ---
|
|
// Ini akan memastikan currentDay maju sesuai waktu kalender jika ESP tidak mati.
|
|
// Jika ESP mati dan menyala kembali, currentDay akan melanjutkan dari lastSavedDay yang dimuat di setup.
|
|
if (isIncubationStarted && overrideDay == 0) { // Hanya saat inkubasi otomatis dan tidak di-override
|
|
DateTime now = rtc.now();
|
|
TimeSpan durasi = now - tanggalMulai;
|
|
int calculatedDay = durasi.days() + 1; // Hitung hari berdasarkan RTC dan tanggalMulai
|
|
|
|
// Jika hari yang dihitung berbeda dari currentDay yang sedang berjalan, update dan simpan
|
|
// Ini memastikan currentDay tidak melompat maju jika ada pemadaman singkat
|
|
if (calculatedDay != currentDay && calculatedDay >= 1 && calculatedDay <= 21) {
|
|
currentDay = calculatedDay;
|
|
simpanHariTerakhir(currentDay); // <--- SIMPAN PERUBAHAN HARI OTOMATIS
|
|
Serial.printf("Hari inkubasi otomatis berubah ke Hari ke-%d. (%s)\n", currentDay, now.timestamp().c_str());
|
|
}
|
|
}
|
|
// Jika tidak di isIncubationStarted, atau ada overrideDay, currentDay akan diatur secara eksplisit.
|
|
// Maka currentDay tidak akan maju otomatis disini.
|
|
|
|
//Serial.println("Debug point: 2");
|
|
|
|
if (nowMillis - lastDHTRead >= DHT_INTERVAL) {
|
|
bacaSensor();
|
|
lastDHTRead = nowMillis;
|
|
}
|
|
|
|
//Serial.println("Debug point: 3");
|
|
|
|
if (isAutoMode) {
|
|
//heaterState = suhu < 37.3;
|
|
|
|
//if (heaterState) {
|
|
// targetPwmDuty = MAX_PWM_DUTY;
|
|
//} else {
|
|
// if (suhu >= 37.7) {
|
|
// targetPwmDuty = 0;
|
|
// } else {
|
|
//
|
|
// }
|
|
//}
|
|
|
|
updateHeaterPWM();
|
|
|
|
isHeaterOn = (targetPwmDuty > 0);
|
|
|
|
digitalWrite(FAN_PIN, isHeaterOn ? HIGH : LOW); // Fan 1 nyala saat heater nyala
|
|
|
|
bool fan2State = false;
|
|
if (suhu > 38.0 || kelembapan > 70 ) {
|
|
fan2State = true;
|
|
}
|
|
else if (suhu < 37.6 && kelembapan < 68) {
|
|
fan2State = false; // Set kipas mati
|
|
}
|
|
digitalWrite(FAN_PIN_2, fan2State ? HIGH : LOW);
|
|
|
|
float targetRH = (currentDay <= 18) ? 55 : 68; // Target RH 55% untuk hari 1-18, 68% untuk hari 19-21
|
|
float turnOnRH;
|
|
if (targetRH == 55) {
|
|
turnOnRH = 53; // Nyala di 53% jika target 55%
|
|
} else { // targetRH == 68
|
|
turnOnRH = 65; // Nyala di 65% jika target 68%
|
|
}
|
|
|
|
if (kelembapan < turnOnRH) {
|
|
mistState = true;
|
|
}
|
|
else if (kelembapan >= targetRH) {
|
|
mistState = false;
|
|
}
|
|
digitalWrite(MISTMAKER_PIN, mistState ? HIGH : LOW);
|
|
isMistMakerOn = mistState;
|
|
|
|
switch (currentDay) { // currentDay sudah diatur di setup atau diperbarui di loop
|
|
case 1: case 2: case 3:
|
|
servoAngleByDay = 40;
|
|
break;
|
|
case 4:
|
|
servoAngleByDay = 60;
|
|
break;
|
|
case 5:
|
|
servoAngleByDay = 85;
|
|
break;
|
|
case 6:
|
|
servoAngleByDay = 110;
|
|
break;
|
|
case 7: case 8: case 9: case 10: case 11: case 12: case 13: case 14: case 15: case 16: case 17:
|
|
servoAngleByDay = 135;
|
|
break;
|
|
case 18: case 19: case 20: case 21:
|
|
servoAngleByDay = 40; // Kembali ke 40 derajat saat periode penetasan
|
|
break;
|
|
default:
|
|
servoAngleByDay = 40; // Default untuk hari di luar range
|
|
break;
|
|
}
|
|
|
|
//Serial.println("Debug point: 4");
|
|
|
|
if (currentServoPos != servoAngleByDay) { // Cek apakah posisi target berbeda dari posisi saat ini
|
|
if (bolehGerakKe(servoAngleByDay)) { // Cek apakah servo diizinkan untuk bergerak
|
|
if (!myServo.attached()) { // Pastikan servo terhubung (attached)
|
|
myServo.attach(SERVO_PIN);
|
|
Serial.println("Servo di-ATTACH"); // Debugging: Konfirmasi attach
|
|
}
|
|
myServo.write(servoAngleByDay);
|
|
//Serial.printf("✅ Servo ventilasi digerakkan ke %d°\n", servoAngleByDay); // Konfirmasi perintah gerakan
|
|
Serial.print("✅ Servo ventilasi digerakkan ke "); // Konfirmasi perintah gerakan
|
|
Serial.println(servoAngleByDay);
|
|
delay(200);
|
|
myServo.detach();
|
|
Serial.println("Servo di-DETACH"); // Debugging: Konfirmasi detach
|
|
currentServoPos = servoAngleByDay; // Perbarui variabel posisi servo saat ini
|
|
detachServoFlag = true;
|
|
detachServoTick = millis();
|
|
} else {
|
|
Serial.println("🚫 Gerakan servo ditolak karena limit switch atau sudut tidak valid.");
|
|
}
|
|
}else{
|
|
if (detachServoFlag){
|
|
if (millis()-detachServoTick>=500){
|
|
detachServoFlag = false;
|
|
myServo.detach();
|
|
Serial.println("Servo di-DETACH"); // Debugging: Konfirmasi detach
|
|
}
|
|
}
|
|
}
|
|
} else { // Mode MANUAL
|
|
targetPwmDuty = 0;
|
|
updateHeaterPWM();
|
|
isHeaterOn = false;
|
|
digitalWrite(FAN_PIN, LOW);
|
|
digitalWrite(FAN_PIN_2, LOW);
|
|
digitalWrite(MISTMAKER_PIN, LOW);
|
|
isMistMakerOn = false;
|
|
myServo.write(40); // Setel servo ke posisi default di mode manual
|
|
currentServoPos = 40;
|
|
}
|
|
|
|
//Serial.println("Debug point: 5");
|
|
|
|
if (nowMillis - lastStatusPrint >= STATUS_PRINT_INTERVAL) {
|
|
//Serial.println("Debug point: 5.1");
|
|
tampilkanStatus();
|
|
//Serial.println("Debug point: 5.2");
|
|
lastStatusPrint = nowMillis;
|
|
}
|
|
|
|
//Serial.println("Debug point: 5.10");
|
|
|
|
if (nowMillis - lastLcdUpdate >= LCD_UPDATE_INTERVAL) {
|
|
//if (Wire.endTransmission()){
|
|
lcd.clear();
|
|
lcd.setCursor(0, 0);
|
|
lcd.printf("S: %.1fC K:%.0f%%", suhu, kelembapan);
|
|
lcd.setCursor(0, 1);
|
|
|
|
if (isWiFiConnected) {
|
|
lcd.printf("Hari: %d | WiFi OK", currentDay);
|
|
} else {
|
|
lcd.printf("Hari: %d | No WiFi", currentDay);
|
|
}
|
|
|
|
if (nowMillis - lastWiFiReconnectAttempt >= WIFI_RECONNECT_INTERVAL) {
|
|
connectToWiFi();
|
|
lastWiFiReconnectAttempt = nowMillis;
|
|
}
|
|
//}
|
|
Wire.endTransmission();
|
|
|
|
lastLcdUpdate = nowMillis;
|
|
}
|
|
|
|
//Serial.println("Debug point: 6");
|
|
|
|
if (nowMillis - lastTickUpdate >= 1000){
|
|
if (isIncubationStarted && overrideDay == 0) {
|
|
DateTime now = rtc.now();
|
|
TimeSpan durasi = now - tanggalMulai;
|
|
int calculatedDay = durasi.days() + 1;
|
|
|
|
if (calculatedDay != currentDay && calculatedDay >= 1 && calculatedDay <= 21) {
|
|
saveDailyHistory(calculatedDay - 1, suhu, kelembapan, currentServoPos);
|
|
|
|
currentDay = calculatedDay;
|
|
simpanHariTerakhir(currentDay); // <--- SIMPAN PERUBAHAN HARI OTOMATIS
|
|
//Serial.printf("Hari inkubasi otomatis berubah ke Hari ke-%d. (%s)\n", currentDay, now.timestamp().c_str());
|
|
String s = "Hari inkubasi otomatis berubah ke Hari ke-" + String(currentDay);
|
|
Serial.println(s);
|
|
}
|
|
}
|
|
lastTickUpdate = nowMillis;
|
|
}
|
|
|
|
//Serial.println("Debug point: 7");
|
|
|
|
RotateRoutine();
|
|
|
|
//Serial.println("Debug point: 8");
|
|
}
|
|
|
|
void matikanSemua() {
|
|
targetPwmDuty = MAX_PWM_DUTY; //set to max again after off
|
|
currentPwmDuty = 0;
|
|
ledcWrite(HEATER_PWM_CHANNEL, 0);
|
|
Serial.println("Heater dimatikan.");
|
|
|
|
digitalWrite(FAN_PIN, LOW);
|
|
digitalWrite(FAN_PIN_2, LOW);
|
|
Serial.println("Fan dimatikan.");
|
|
digitalWrite(MISTMAKER_PIN, LOW);
|
|
Serial.println("Mistmaker dimatikan.");
|
|
myServo.write(40); // Setel servo ke posisi default
|
|
currentServoPos = 40;
|
|
Serial.println("Servo ventilasi disetel ke 40°.");
|
|
|
|
RotateZero();
|
|
Serial.println("Rak telur tegak");
|
|
|
|
Serial.println("Semua aktuator dimatikan.");
|
|
}
|
|
|
|
void tampilkanStatus() {
|
|
//Serial.println("Debug point -> tampilkanStatus: 1");
|
|
|
|
String rackAngleString = "";
|
|
rackAngleString = "Belum inisiasi";
|
|
if (posisiRakMiring()) rackAngleString = "Miring";
|
|
if (posisiRakTegak()) rackAngleString = "Tegak";
|
|
int pwmDisplayValue = (currentPwmDuty*100) / MAX_PWM_DUTY;
|
|
|
|
// Serial.printf("Suhu: %.2f°C, RH: %.2f%%, Hari inkubasi: %d, Sudut ventilasi: %d°, Heater PWM: %d, Fan: %s, Mistmaker: %s, Motor: %s | Mode Sistem: %s | Inkubasi: %s | WiFi: %s\n",
|
|
// suhu, kelembapan, currentDay, currentServoPos, pwmDisplayValue,
|
|
// heaterState ? "ON" : "OFF",
|
|
// mistState ? "ON" : "OFF",
|
|
// isAutoMode ? "OTOMATIS" : "MANUAL",
|
|
// isIncubationStarted ? "DIMULAI" : "BELUM DIMULAI", // Tambahkan status inkubasi
|
|
// isWiFiConnected ? "TERHUBUNG" : "TERPUTUS");
|
|
Serial.print("Suhu: " + String(suhu, 2) + "°C, ");
|
|
Serial.print("RH: " + String(kelembapan, 2) + "%, ");
|
|
Serial.print("Hari inkubasi: " + String(currentDay) + ", ");
|
|
Serial.print("Sudut ventilasi: " + String(currentServoPos) + ", ");
|
|
Serial.print("Heater PWM: " + String(pwmDisplayValue) + ", ");
|
|
Serial.print("posisi rak: " + rackAngleString + ", ");
|
|
Serial.print("Motor Rak: " + String(motorNyala ? "ON" : "OFF") + ", ");
|
|
|
|
Serial.println();
|
|
//Serial.println("Debug point -> tampilkanStatus: 2");
|
|
}
|
|
|
|
bool bolehGerakKe(int targetAngle) {
|
|
if (digitalRead(LIMIT_SWITCH_PIN) == LOW) {
|
|
Serial.println("Limit switch tertekan, gerakan servo dicegah.");
|
|
return false;
|
|
}
|
|
// Batasi rentang gerak servo
|
|
if (targetAngle < 40 || targetAngle > 135) {
|
|
Serial.printf("Sudut target %d° di luar rentang yang diizinkan (40-135°).\n", targetAngle);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
|
|
//urusan rak telur
|
|
void setupRakTelur() {
|
|
Serial.println("Melakukan Setup Rak Telur...");
|
|
pinMode(HALL_DO_PIN, INPUT);
|
|
pinMode(TRIAC_PIN, OUTPUT);
|
|
//Wire.begin(32, 33);
|
|
encoder.begin();
|
|
lastStartTime = millis();
|
|
|
|
if (isIncubationStarted) {
|
|
if (currentDay >= 19) {
|
|
RotateZero();
|
|
} else {
|
|
RotateAgain();
|
|
}
|
|
} else {
|
|
RotateZero();
|
|
Serial.println("Menegakkan rak telur.");
|
|
}
|
|
tRotate = millis();
|
|
Serial.println("Setup Rak Telur OK.");
|
|
}
|
|
|
|
void SetMotor(bool state) {
|
|
if (motorNyala != state) {
|
|
motorNyala = state;
|
|
digitalWrite(TRIAC_PIN, state ? HIGH : LOW);
|
|
Serial.println("Motor: " + (String) (state ? "ON" : "OFF"));
|
|
|
|
}
|
|
}
|
|
|
|
bool RotateZero() {
|
|
// int raw = encoder.rawAngle();
|
|
// uint8_t i2cError = Wire1.lastError();
|
|
// if (i2cError != 0) {
|
|
// SetMotor(false);
|
|
// rotateState = ROT_NONE;
|
|
// rotateState = rotateStateTarget;
|
|
// Serial.println("Gagal membaca sensor sudut!");
|
|
// return false;
|
|
// }
|
|
|
|
if (rotateStateTarget != ROT_ZERO) {
|
|
rotateStateTarget = ROT_ZERO;
|
|
SetMotor(true);
|
|
}
|
|
|
|
//tunggu sampai kena hall sensor
|
|
if (rotateStateTarget != rotateState) {
|
|
bool hall = !((bool)digitalRead(HALL_DO_PIN));
|
|
if (hall) {
|
|
rotateState = rotateStateTarget;
|
|
SetMotor(false);
|
|
sudutZero = encoder.rawAngle();
|
|
sudutZeroDerajat = (sudutZero * 360) / 4096;
|
|
sudutSekarang = 0;
|
|
sudutSekarangDerajat = 0;
|
|
encoder.setOffset(0);
|
|
encoder.resetPosition(0);
|
|
Serial.print("Sudut tengah: raw ");
|
|
Serial.print(sudutZero);
|
|
Serial.print(", derajat ");
|
|
Serial.println(sudutZeroDerajat);
|
|
//tRotate = millis();
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool RotateAgain() {
|
|
if (rotateStateTarget == ROT_SKEW) {
|
|
if (rotateState == ROT_SKEW) {
|
|
rotateState = ROT_NONE;
|
|
rotateStateTarget = ROT_NONE;
|
|
if (putarRakByTimer) {
|
|
RotateByTimer();
|
|
} else {
|
|
Rotate();
|
|
}
|
|
Serial.println("Memutar rak...");
|
|
return true;
|
|
}
|
|
} else if ((rotateState == ROT_NONE && rotateStateTarget == ROT_NONE) || (rotateState == ROT_ZERO && rotateStateTarget == ROT_ZERO) ) {
|
|
if (putarRakByTimer) {
|
|
RotateByTimer();
|
|
} else {
|
|
Rotate();
|
|
}
|
|
Serial.println("Memutar rak...");
|
|
return true;
|
|
}
|
|
Serial.println("Gagal memutar rak!");
|
|
Serial.println("rotateState: " + String(rotateState));
|
|
Serial.println("rotateStateTarget: " + String(rotateStateTarget));
|
|
return false;
|
|
}
|
|
|
|
bool RotateByTimer() {
|
|
if (rotateStateTarget != ROT_SKEW) {
|
|
rotateStateTarget = ROT_SKEW;
|
|
bool hall = !((bool)digitalRead(HALL_DO_PIN));
|
|
if (hall) {
|
|
sudutZero = encoder.rawAngle();
|
|
sudutZeroDerajat = (sudutZero * 360) / 4096;
|
|
rotateFindZero = false;
|
|
encoder.setOffset(0);
|
|
encoder.resetPosition(0);
|
|
Serial.print("Sudut tengah: raw ");
|
|
Serial.print(sudutZero);
|
|
Serial.print(", derajat ");
|
|
Serial.println(sudutZeroDerajat);
|
|
} else {
|
|
rotateFindZero = true;
|
|
Serial.println("Cari sudut tengah dulu...");
|
|
}
|
|
tStopPutar = millis();
|
|
SetMotor(true);
|
|
}
|
|
|
|
bool hall = false;
|
|
if (rotateStateTarget != rotateState) {
|
|
if (rotateFindZero) {
|
|
hall = !((bool)digitalRead(HALL_DO_PIN));
|
|
if (hall) {
|
|
rotateFindZero = false;
|
|
sudutZero = encoder.rawAngle();
|
|
sudutZeroDerajat = (sudutZero * 360) / 4096;
|
|
encoder.setOffset(0);
|
|
encoder.resetPosition(0);
|
|
Serial.print("Update Sudut tengah: raw ");
|
|
Serial.print(sudutZero);
|
|
Serial.print(", derajat ");
|
|
Serial.println(sudutZeroDerajat);
|
|
tStopPutar = millis();
|
|
}
|
|
} else {
|
|
hall = !((bool)digitalRead(HALL_DO_PIN));
|
|
if (hall) {
|
|
rotateFindZero = false;
|
|
sudutZero = encoder.rawAngle();
|
|
sudutZeroDerajat = (sudutZero * 360) / 4096;
|
|
encoder.setOffset(0);
|
|
encoder.resetPosition(0);
|
|
tStopPutar = millis();
|
|
}
|
|
sudutSekarang = encoder.rawAngle();
|
|
sudutSekarangDerajat = abs(((sudutSekarang * 360) / 4096) - sudutZeroDerajat);
|
|
float angular = encoder.getAngularSpeed();
|
|
Serial.print("Sudut: ");
|
|
Serial.print(sudutSekarangDerajat);
|
|
Serial.print(", angular speed ");
|
|
Serial.println(angular);
|
|
if ((sudutSekarangDerajat >= 45) || (millis() - tStopPutar >= 7000)) {
|
|
rotateState = rotateStateTarget;
|
|
SetMotor(false);
|
|
tRotate = millis();
|
|
Serial.println("Berhenti.");
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool Rotate() {
|
|
// int raw = encoder.rawAngle();
|
|
// uint8_t i2cError = Wire1.lastError();
|
|
// if (i2cError != 0) {
|
|
// SetMotor(false);
|
|
// rotateState = ROT_NONE;
|
|
// rotateState = rotateStateTarget;
|
|
// Serial.println("Gagal membaca sensor sudut!");
|
|
// return false;
|
|
// }
|
|
|
|
if (rotateStateTarget != ROT_SKEW) {
|
|
rotateStateTarget = ROT_SKEW;
|
|
bool hall = !((bool)digitalRead(HALL_DO_PIN));
|
|
if (hall) {
|
|
sudutZero = encoder.rawAngle();
|
|
sudutZeroDerajat = (sudutZero * 360) / 4096;
|
|
rotateFindZero = false;
|
|
encoder.setOffset(0);
|
|
encoder.resetPosition(0);
|
|
Serial.print("Sudut tengah: raw ");
|
|
Serial.print(sudutZero);
|
|
Serial.print(", derajat ");
|
|
Serial.println(sudutZeroDerajat);
|
|
} else {
|
|
rotateFindZero = true;
|
|
Serial.println("Cari sudut tengah dulu...");
|
|
}
|
|
SetMotor(true);
|
|
}
|
|
|
|
bool hall = false;
|
|
if (rotateStateTarget != rotateState) {
|
|
if (rotateFindZero) {
|
|
hall = !((bool)digitalRead(HALL_DO_PIN));
|
|
if (hall) {
|
|
rotateFindZero = false;
|
|
sudutZero = encoder.rawAngle();
|
|
sudutZeroDerajat = (sudutZero * 360) / 4096;
|
|
encoder.setOffset(0);
|
|
encoder.resetPosition(0);
|
|
Serial.print("Update Sudut tengah: raw ");
|
|
Serial.print(sudutZero);
|
|
Serial.print(", derajat ");
|
|
Serial.println(sudutZeroDerajat);
|
|
}
|
|
} else {
|
|
hall = !((bool)digitalRead(HALL_DO_PIN));
|
|
if (hall) {
|
|
rotateFindZero = false;
|
|
sudutZero = encoder.rawAngle();
|
|
sudutZeroDerajat = (sudutZero * 360) / 4096;
|
|
encoder.setOffset(0);
|
|
encoder.resetPosition(0);
|
|
}
|
|
sudutSekarang = encoder.rawAngle();
|
|
sudutSekarangDerajat = abs(((sudutSekarang * 360) / 4096) - sudutZeroDerajat);
|
|
float angular = encoder.getAngularSpeed();
|
|
Serial.print("Sudut: ");
|
|
Serial.print(sudutSekarangDerajat);
|
|
Serial.print(", angular speed ");
|
|
Serial.println(angular);
|
|
if (sudutSekarangDerajat >= 45) {
|
|
rotateState = rotateStateTarget;
|
|
SetMotor(false);
|
|
tRotate = millis();
|
|
Serial.println("Berhenti.");
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void RotateRoutine() {
|
|
if (isAutoMode && isIncubationStarted) {
|
|
//if (true) {
|
|
if (!motorNyala) {
|
|
if (currentDay <= 19) {
|
|
if (((millis() - tRotate) >= jedaPutarRak) || posisiRakTegak()) {
|
|
//Serial.println("debug r3");
|
|
tRotate = millis();
|
|
RotateAgain();
|
|
Serial.println("Memutar rak telur...");
|
|
}
|
|
} else {
|
|
RotateZero();
|
|
}
|
|
}
|
|
}
|
|
|
|
if (rotateStateTarget != rotateState) {
|
|
if (rotateStateTarget == ROT_ZERO) {
|
|
RotateZero();
|
|
} else if (rotateStateTarget == ROT_SKEW) {
|
|
if (putarRakByTimer) {
|
|
RotateByTimer();
|
|
} else {
|
|
Rotate();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
bool posisiRakMiring() {
|
|
return (rotateState == ROT_SKEW);
|
|
}
|
|
|
|
bool posisiRakTegak() {
|
|
return (rotateState == ROT_ZERO);
|
|
}
|