/*
* PROFESYONEL KULUÇKA MAKİNESİ KONTROL SİSTEMİ - Rev 3.5 I2C & Joystick Mod
* LCD I2C Kullanımı, Joystick Navigasyon, Tarih Ayarı, Negatif Polarite Rolleri ve Rotasyon Süresi Gösterimi
* Gelişmiş Alarm Seviyeleri ve Snooze Özelliği Eklendi
*/
#include <DHT.h>
#include <Wire.h>
#include <RTClib.h>
#include <LiquidCrystal_I2C.h>
#include <PID_v1.h>
#include <EEPROM.h>
// ==================== ENUM & DONANIM TANIMLARI ====================
enum Button { BTN_NONE, BTN_RIGHT, BTN_UP, BTN_DOWN, BTN_LEFT, BTN_SELECT };
#define DHTPIN A1
#define DHTTYPE DHT22
#define JOY_X A0
#define JOY_Y A2
#define JOY_SW 2
#define FAN_PIN 3
#define MOTOR_PIN 4
#define RELAY_HEAT 11 // Aktif düşük
#define RELAY_HUMID 12 // Aktif düşük
#define RELAY_COOL 13 // Aktif düşük
#define BUZZER_PIN 5
float lastTemp = -1000;
int lastHum = -1;
int lastGun = -1;
int lastAlarmLevel = -1;
String lastTimeStr = "";
int lastMenu = -1;
bool lastEditing = false;
// ==================== PID KONFİGÜRASYON ====================
struct PIDConfig { double Kp, Ki, Kd; };
const PIDConfig TEMP_PID[4] = {{1.8,0.4,0.9},{2.0,0.5,1.1},{2.2,0.3,0.8},{1.5,0.6,1.0}};
const PIDConfig HUM_PID[4] = {{1.2,0.2,0.5},{1.5,0.3,0.7},{1.8,0.4,0.6},{2.0,0.5,0.9}};
// ==================== TÜRLER KONFİGÜRASYON ====================
struct SpeciesConfig { const char* name; int incubationDays; float tNMin,tNMax,tFMin,tFMax; float hNMin,hNMax,hFMin,hFMax; int rotIntH,rotDurM,fanSpeed,coolingDay; };
const SpeciesConfig SPECIES[4] = {
{"Tavuk",21,37.5,38.0,37.2,37.5,50,60,70,75,3,5,200,-1},
{"Hindi",28,37.3,37.6,37.0,37.3,55,65,75,80,4,7,220,-1},
{"Bildircin",17,37.7,38.0,37.5,37.7,55,60,70,75,2,3,255,-1},
{"Kaz",30,37.3,37.6,36.9,37.2,60,65,80,85,6,10,255,25}
};
// ==================== DURUM VE GLOBAL DEĞİŞKENLER ====================
struct State {
uint8_t species; DateTime startTime;
unsigned long lastRot, rotStart;
bool rotActive; } state;
double setTemp, curTemp, outHeat;
double setHum, curHum, outHum;
bool alarmActive = false;
// Yeni global değişkenler: alarm seviyesi ve snooze
uint8_t alarmLevel = 0; // 0 = yok, 1 = uyarı, 2 = alarm, 3 = kritik
unsigned long snoozeUntil = 0; // millis() tabanlı erteleme süresi
LiquidCrystal_I2C lcd(0x27,16,2);
DHT dht(DHTPIN,DHTTYPE);
RTC_DS3231 rtc;
PID pidT(&curTemp,&outHeat,&setTemp,TEMP_PID[0].Kp,TEMP_PID[0].Ki,TEMP_PID[0].Kd,DIRECT);
PID pidH(&curHum,&outHum,&setHum, HUM_PID[0].Kp, HUM_PID[0].Ki, HUM_PID[0].Kd,DIRECT);
// ==================== MENÜ YAPISI ====================
enum MenuItem{HOME,MENU_SPECIES,ROT_INT,ROT_DUR,FAN,DATE,PID_INFO};
const char* MENU[] = {"AnaEkran","Tur","Arlk","Sure","Fan","Tarih","PID"};
MenuItem curMenu = HOME; bool editing = false;
enum DateField { DF_DAY, DF_MON, DF_YEAR }; DateField df = DF_DAY;
enum Addr { A_SP=0, A_Y=10, A_M=11, A_D=12 };
// ==================== PROTOTİPLER ====================
void initHW();
void loadS();
void saveS();
void readSensors();
void calcPID();
void ctrlOut();
void handleRot(); // Prototip
void checkAlarms();
long getRotRemainingMs();
void draw();
void nav(Button b);
void edit(Button b);
void editDate(Button b);
void setDate();
int daysLeft();
Button readBtn();
// ==================== SETUP & LOOP ====================
void setup() {
initHW();
loadS();
pidT.SetMode(AUTOMATIC);
pidH.SetMode(AUTOMATIC);
}
void loop() {
Button b = readBtn();
// 1) Eğer alarm seviyesi yüksekse ve SELECT'e basıldıysa, ertele:
if (curMenu == HOME && b == BTN_SELECT && alarmLevel > 0) {
snoozeUntil = millis() + 300000UL; // 5 dk ertele
b = BTN_NONE;
}
// 2) Menü navigasyonu / edit modu
if (!editing) {
nav(b);
} else {
edit(b);
}
// 3) Temel işlevler
readSensors();
calcPID();
ctrlOut();
handleRot();
checkAlarms();
draw();
delay(200);
}
// ==================== DONANIM BAŞLAT ====================
void initHW() {
Wire.begin(); lcd.begin(16,2); lcd.backlight(); dht.begin();
if (!rtc.begin()) { lcd.clear(); lcd.print("RTC HATA"); while (1); }
pinMode(JOY_SW, INPUT_PULLUP);
pinMode(RELAY_HEAT, OUTPUT); pinMode(RELAY_HUMID, OUTPUT); pinMode(RELAY_COOL, OUTPUT);
pinMode(MOTOR_PIN, OUTPUT); pinMode(FAN_PIN, OUTPUT); pinMode(BUZZER_PIN, OUTPUT);
// Negatif polarite: HIGH = kapalı, LOW = aktif
digitalWrite(RELAY_HEAT, HIGH);
digitalWrite(RELAY_HUMID, HIGH);
digitalWrite(RELAY_COOL, HIGH);
digitalWrite(MOTOR_PIN, HIGH); analogWrite(FAN_PIN, 0); digitalWrite(BUZZER_PIN, LOW);
}
// ==================== EEPROM YÜKLE & KAYDET ====================
void handleRot() {
auto &c = SPECIES[state.species];
if (daysLeft() <= 3) return;
unsigned long now = millis();
if (!state.rotActive && now - state.lastRot > c.rotIntH * 3600000UL) {
state.rotActive = true;
state.rotStart = now;
state.lastRot = now;
digitalWrite(MOTOR_PIN, LOW);
}
if (state.rotActive && now - state.rotStart > c.rotDurM * 60000UL) {
state.rotActive = false;
digitalWrite(MOTOR_PIN, HIGH);
}
}
void loadS() {
uint8_t sp = EEPROM.read(A_SP);
if (sp < 4) state.species = sp;
int y = EEPROM.read(A_Y) + 2000;
int m = EEPROM.read(A_M);
int d = EEPROM.read(A_D);
if (y > 2000 && m>=1 && m<=12 && d>=1 && d<=31) {
state.startTime = DateTime(y,m,d,0,0,0);
} else {
state.startTime = rtc.now();
saveS();
}
}
void saveS() {
EEPROM.update(A_SP, state.species);
EEPROM.update(A_Y, state.startTime.year() - 2000);
EEPROM.update(A_M, state.startTime.month());
EEPROM.update(A_D, state.startTime.day());
}
int daysLeft() {
long diff = rtc.now().unixtime() - state.startTime.unixtime();
int rem = SPECIES[state.species].incubationDays - diff/86400;
return rem<0?0:rem;
}
void readSensors() {
static float lt, lh; float t=dht.readTemperature(), h=dht.readHumidity();
curTemp = isnan(t)?lt:(lt=t);
curHum = isnan(h)?lh:(lh=h);
}
void calcPID() {
auto &c = SPECIES[state.species]; bool fin = daysLeft()<=3;
pidT.SetTunings(TEMP_PID[state.species].Kp, TEMP_PID[state.species].Ki, TEMP_PID[state.species].Kd);
pidH.SetTunings(HUM_PID[state.species].Kp, HUM_PID[state.species].Ki, HUM_PID[state.species].Kd);
setTemp = fin?(c.tFMin+c.tFMax)/2:(c.tNMin+c.tNMax)/2;
setHum = fin?(c.hFMin+c.hFMax)/2:(c.hNMin+c.hNMax)/2;
pidT.Compute(); pidH.Compute();
}
void ctrlOut() {
auto &c=SPECIES[state.species]; analogWrite(FAN_PIN,c.fanSpeed);
digitalWrite(RELAY_HEAT, outHeat>0?LOW:HIGH);
digitalWrite(RELAY_HUMID,outHum>0?LOW:HIGH);
bool coolOn = (state.species==3 && daysLeft()==c.coolingDay);
digitalWrite(RELAY_COOL, coolOn?LOW:HIGH);
}
// ==================== ALARM & SNOOZE ====================
void checkAlarms() {
auto &c = SPECIES[state.species];
bool fin = daysLeft() <= 3;
float tmin = fin ? c.tFMin : c.tNMin;
float tmax = fin ? c.tFMax : c.tNMax;
float hmin = fin ? c.hFMin : c.hNMin;
float hmax = fin ? c.hFMax : c.hNMax;
// Sapma hesaplama
float deltaT = 0, deltaH = 0;
if (curTemp < tmin) deltaT = tmin - curTemp;
else if (curTemp > tmax) deltaT = curTemp - tmax;
if (curHum < hmin) deltaH = hmin - curHum;
else if (curHum > hmax) deltaH = curHum - hmax;
float maxDelta = max(deltaT, deltaH);
// Alarm seviyesi belirleme
if (maxDelta > 2.0) alarmLevel = 3;
else if (maxDelta > 1.0) alarmLevel = 2;
else if (maxDelta > 0.5) alarmLevel = 1;
else alarmLevel = 0;
// Snooze kontrolü
if (millis() < snoozeUntil) { noTone(BUZZER_PIN); return; }
// Sesli uyarı
switch (alarmLevel) {
case 1: tone(BUZZER_PIN, 500, 200); break; // kısa uyarı bip
case 2: tone(BUZZER_PIN, 1000); break; // sürekli alarm
case 3: // kritik: tüm ısıtma/soğutma kapat
digitalWrite(RELAY_HEAT, HIGH);
digitalWrite(RELAY_COOL, HIGH);
tone(BUZZER_PIN, 2000);
break;
default: noTone(BUZZER_PIN); break;
}
}
long getRotRemainingMs(){
auto &c=SPECIES[state.species]; unsigned long now=millis();
if(state.rotActive){ long rem=c.rotDurM*60000UL-(now-state.rotStart); return rem>0?rem:0; }
long rem=c.rotIntH*3600000UL-(now-state.lastRot); return rem>0?rem:0;
}
void draw() {
if (curMenu == HOME) {
bool needsUpdate = false;
if (curTemp != lastTemp || (int)curHum != lastHum || daysLeft() != lastGun || alarmLevel != lastAlarmLevel)
needsUpdate = true;
long remMs = getRotRemainingMs(); int h = remMs / 3600000; int m = (remMs % 3600000) / 60000;
char buf[6]; snprintf(buf, sizeof(buf), "%2d:%02d", h, m);
String newTimeStr = String(buf);
if (newTimeStr != lastTimeStr) needsUpdate = true;
if (!needsUpdate) return;
lcd.clear();
lcd.setCursor(0, 0);
lcd.print(curTemp, 1);
lcd.write(223);
lcd.print("C ");
lcd.print((int)curHum);
lcd.print("% G");
lcd.print(daysLeft());
if (alarmLevel > 0) {
lcd.setCursor(0, 1);
lcd.print("ALRM Lv"); lcd.print(alarmLevel);
} else {
lcd.setCursor(16 - newTimeStr.length(), 1); lcd.print(newTimeStr);
}
lastTemp = curTemp;
lastHum = (int)curHum;
lastGun = daysLeft();
lastAlarmLevel = alarmLevel;
lastTimeStr = newTimeStr;
}
else {
// Sadece menu değiştiyse temizle ve yeniden çiz
if (curMenu != lastMenu || editing != lastEditing) {
lcd.clear();
lcd.setCursor(0, 0);
lcd.print(MENU[curMenu]);
if (editing) { lcd.setCursor(15, 0); lcd.print('*'); }
lcd.setCursor(0, 1);
switch (curMenu) {
case MENU_SPECIES: lcd.print(SPECIES[state.species].name); break;
case ROT_INT: lcd.print(SPECIES[state.species].rotIntH); lcd.print("h"); break;
case ROT_DUR: lcd.print(SPECIES[state.species].rotDurM); lcd.print("m"); break;
case FAN: lcd.print(map(SPECIES[state.species].fanSpeed, 0, 255, 0, 100)); lcd.print("%"); break;
case DATE: {
auto dt = state.startTime;
char buf[11];
snprintf(buf, sizeof(buf), "%02d/%02d/%04d", dt.day(), dt.month(), dt.year());
lcd.print(buf);
break;
}
case PID_INFO:
lcd.print("KP:"); lcd.print(TEMP_PID[state.species].Kp);
lcd.setCursor(0, 1);
lcd.print("KI:"); lcd.print(TEMP_PID[state.species].Ki); lcd.print(" KD:"); lcd.print(TEMP_PID[state.species].Kd);
break;
}
}
lastMenu = curMenu;
lastEditing = editing;
}
}
void nav(Button b){
if(b==BTN_UP) curMenu=(curMenu==HOME?PID_INFO:MenuItem(curMenu-1));
if(b==BTN_DOWN) curMenu=(curMenu==PID_INFO?HOME:MenuItem(curMenu+1));
if(b==BTN_SELECT && curMenu!=HOME){editing=true; if(curMenu==DATE) df=DF_DAY;}
}
void edit(Button b){
if(curMenu==DATE) editDate(b);
else if(curMenu==MENU_SPECIES && (b==BTN_LEFT||b==BTN_RIGHT)){
state.species=(state.species+(b==BTN_LEFT?3:1))%4; state.startTime=rtc.now(); saveS();
}
if(b==BTN_SELECT){ editing=false; if(curMenu==MENU_SPECIES||curMenu==DATE) saveS(); }
}
Button readBtn(){
int x=analogRead(JOY_X), y=analogRead(JOY_Y);
if(digitalRead(JOY_SW)==LOW) return BTN_SELECT;
if(x<300) return BTN_LEFT; if(x>700) return BTN_RIGHT;
if(y<300) return BTN_UP; if(y>700) return BTN_DOWN;
return BTN_NONE;
}
void editDate(Button b){
int y=state.startTime.year(), m=state.startTime.month(), d=state.startTime.day();
if(b==BTN_LEFT) df=DateField((df+2)%3);
else if(b==BTN_RIGHT) df=DateField((df+1)%3);
else if(b==BTN_UP||b==BTN_DOWN){ int delta=(b==BTN_UP?1:-1);
if(df==DF_DAY) d=constrain(d+delta,1,31);
else if(df==DF_MON) m=constrain(m+delta,1,12);
else y=constrain(y+delta,2000,2099);
state.startTime=DateTime(y,m,d,0,0,0);
}
}
void setDate(){
int y=state.startTime.year(), m=state.startTime.month(), d=state.startTime.day(), s=0;
while(1){
lcd.clear(); lcd.setCursor(0,0); lcd.print("Tarih Ayari:"); lcd.setCursor(0,1);
lcd.print(s==0?">":" "); if(d<10) lcd.print('0'); lcd.print(d); lcd.print("/");
lcd.print(s==1?">":" "); if(m<10) lcd.print('0'); lcd.print(m); lcd.print("/");
lcd.print(s==2?">":" "); lcd.print(y);
Button btn=readBtn(); if(btn==BTN_UP||btn==BTN_DOWN||btn==BTN_LEFT||btn==BTN_RIGHT) editDate(btn);
else if(btn==BTN_SELECT){ s++; if(s>2){ rtc.adjust(DateTime(y,m,d,0,0,0)); state.startTime=DateTime(y,m,d,0,0,0); saveS(); editing=false; break; }}
delay(200);
}
}