/*
* PROFESYONEL KULUÇKA MAKİNESİ KONTROL SİSTEMİ - Rev 3.5 I2C & Joystick & SHT31 Mod
* LCD I2C, Joystick Navigasyon, Tarih Ayarı, Negatif Polarite Röleleri,
* Rotasyon Süresi Gösterimi, Gelişmiş Alarm Seviyeleri ve Snooze Özelliği...
* DHT22 yerine SHT31 kullanımıyla ölçüm kararlılığı artırıldı.
*/
#include <Wire.h>
#include <RTClib.h>
#include <LiquidCrystal_I2C.h>
#include <PID_v1.h>
#include <EEPROM.h>
#include "Adafruit_SHT31.h"
// ==================== DONANIM TANIMLARI ====================
enum Button { BTN_NONE, BTN_RIGHT, BTN_UP, BTN_DOWN, BTN_LEFT, BTN_SELECT };
#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
// SHT31 sensör nesnesi
Adafruit_SHT31 sht31 = Adafruit_SHT31();
LiquidCrystal_I2C lcd(0x27,16,2);
RTC_DS3231 rtc;
// Son değerler (LCD güncelleme optimizasyonu için)
double lastTemp = -1000;
int lastHum = -1;
int lastGun = -1;
int lastAlarm = -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}
};
// ==================== 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;
uint8_t alarmLevel = 0;
unsigned long snoozeUntil = 0;
// PID kontrol objeleri
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 };
// Prototipler
void initHW();
void loadS();
void saveS();
void readSensors();
void calcPID();
void ctrlOut();
void handleRot();
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 ====================
void setup() {
Wire.begin();
lcd.begin(16,2);
lcd.backlight();
// SHT31 Başlatma
if (!sht31.begin(0x44)) {
lcd.clear(); lcd.print("SHT31 HATA");
while (1) delay(10);
}
// RTC Başlatma
if (!rtc.begin()) {
lcd.clear(); lcd.print("RTC HATA");
while (1) delay(10);
}
initHW();
loadS();
pidT.SetMode(AUTOMATIC);
pidH.SetMode(AUTOMATIC);
}
// ==================== LOOP ====================
void loop() {
Button b = readBtn();
// ANTELE: Erteleme
if (curMenu == HOME && b == BTN_SELECT && alarmLevel > 0) {
snoozeUntil = millis() + 300000UL; // 5 dk ertele
b = BTN_NONE;
}
// Menü kontrol
if (!editing) nav(b);
else edit(b);
// Temel işlevler
readSensors();
calcPID();
ctrlOut();
handleRot();
checkAlarms();
draw();
delay(200);
}
// ==================== DONANIM BAŞLAT ====================
void initHW() {
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 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());
}
// ==================== GÜN HESABI ====================
int daysLeft() {
long diff = rtc.now().unixtime() - state.startTime.unixtime();
int rem = SPECIES[state.species].incubationDays - diff/86400;
return rem < 0 ? 0 : rem;
}
// ==================== SENSÖR OKUMA (SHT31) ====================
void readSensors() {
float t = sht31.readTemperature();
float h = sht31.readHumidity();
if (!isnan(t)) curTemp = t;
if (!isnan(h)) curHum = h;
}
// ==================== PID HESAPLAMA ====================
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();
}
// ==================== ÇIKIŞ KONTROL ====================
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);
}
// ==================== ROTASYON ====================
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);
}
}
// ==================== 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;
float dT = 0, dH = 0;
if (curTemp < tmin) dT = tmin - curTemp;
else if (curTemp > tmax) dT = curTemp - tmax;
if (curHum < hmin) dH = hmin - curHum;
else if (curHum > hmax) dH = curHum - hmax;
float mx = max(dT, dH);
alarmLevel =
mx > 2.0 ? 3 :
(mx > 1.0 ? 2 :
(mx > 0.5 ? 1 : 0));
if (millis() < snoozeUntil) {
noTone(BUZZER_PIN);
return;
}
switch (alarmLevel) {
case 1: tone(BUZZER_PIN, 200, 200); break;
case 2: tone(BUZZER_PIN, 500, 100); break;
case 3:
//digitalWrite(RELAY_HEAT, HIGH);
// digitalWrite(RELAY_COOL, HIGH);
tone(BUZZER_PIN, 1000, 50);
break;
default: noTone(BUZZER_PIN); break;
}
}
// ==================== KALAN ROTASYON SÜRESİ ====================
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;
}
// ==================== EKRAN ÇİZİM ====================
void draw() {
if (curMenu == HOME) {
bool needsUpdate = false;
if (curTemp != lastTemp || (int)curHum != lastHum || daysLeft() != lastGun || alarmLevel != lastAlarm)
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 timeStr(buf);
if (timeStr != 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());
lcd.setCursor(0,1);
if (alarmLevel > 0) {
lcd.print("ALRM Lv");
lcd.print(alarmLevel);
} else {
lcd.setCursor(16 - timeStr.length(), 1);
lcd.print(timeStr);
}
lastTemp = curTemp;
lastHum = (int)curHum;
lastGun = daysLeft();
lastAlarm = alarmLevel;
lastTimeStr = timeStr;
} else {
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 dbuf[11];
snprintf(dbuf, sizeof(dbuf), "%02d/%02d/%04d", dt.day(), dt.month(), dt.year());
lcd.print(dbuf);
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;
}
}
// ==================== NAV & EDIT ====================
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();
}
}
// ==================== DATE EDIT ====================
void editDate(Button b) {
int y = state.startTime.year();
int m = state.startTime.month();
int 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);
}
}
// ==================== SET DATE ====================
void setDate() {
int y = state.startTime.year();
int m = state.startTime.month();
int d = state.startTime.day();
int 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_NONE) 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);
}
}
// ==================== BUTTON READ ====================
Button readBtn() {
int x = analogRead(JOY_X);
int 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;
}