ÖZELLİKLER
Kullanıcı Adı:
NightWist
Kullanıcı Grubu:
Forum Üyesi
Geri Bildirimleri:
Aldığı Beğeni:
622
Hesap Durumu:
Aktif
Durumu:
Çevrim Dışı
Üyelik Tarihi:
28 Haziran 2023 23:26
Son Ziyaret:
1 saat 59 dakika önce
Toplam Mesaj:
890 [0.73 Gün Ortalaması]
Paylaşım Sayisı:
71 (Son 6 Ay)
İlan Sayisı:
BİLGİLER
Ad Soyad:
Burak Çetin
Doğum Tarihi:
21 Şubat 2007
Yaş:
19
İl:
Izmir
Meslek:
Öğrenci
Özel Mesaj:
Sohbet Talebi:
Üye Favorile:
Sosyal Medya:
İMZA
Burak Çetin
SON 10 MESAJI
Ternapi Medaka Pondları
[QUOTE=ternapi]
[:D]
Şakası bir yana, balıkları bu konuda hafife alıyor olabiliriz. Bir kere gayet uzun süreli hafızaları olduğunu düşünüyorum.
Mesela medakalarım ve gambusyalarım 10-15 gün yalnız kalıyorlar. Sonrasında ben ya da eşim ponda yaklaştığımız anda hepsi resmen koşa koşa yanımıza geliyor. Normalde içgüdüsel olarak üstten kuş gölgesi geçse dibe kaçıp saklanan canlılar bunlar.
Betta yetiştirdiğim zamanlar ben tanka yaklaştığımda yem verirdim, eşim ise sadece bettayla oyun oynamak için yaklaşırdı. Ben tanka yaklaşınca balık hemen fıtı fıtı gelir, bir süre yem bekler ve yem yoksa totosunu dönüp toz olurdu[:)))] ama tanka yaklaşan eşimse hiç yem beklemeden parmak takip etmece oynamaya başlar ve dakikalarca oynarlardı, kıskanır sinir olurdum[:devil:][:D]
Niyet okumada da çok başarılılar. Elimde kepçeyle ponda yaklaşmalarımın neredeyse tamamı su piresi ya da larva falan vermek için oluyor ve bu şekilde her yaklaşmamda hepsi koşa koşa gelip toplaşırlar ama kırk yılda bir, elimde aynı kepçe, balıklardan birini yakalama niyetiyle yaklaştığımda resmen ışık hızıyla kayboluverir şapşikler[:devil:]
Bu durumu, zihin okuma dışında bir şeyle açıklamak bence çok zor[:cik:][/QUOTE]
Eski betam aynısıydı. Parmağımı soktuğum anda kendini sevdirmeye geliyordu. Yem vereceğim zaman da aynı şekilde. Bazen ailem verirdi yemi ben uyuya kaldığım için yemezdi o yemleri. Sabah akvaryum dibinden geri toplardım. Canlı yem bile verseler nazlanıyordu ben olmayınca. Hissediyor ve tanıyorlar gerçekten.
[QUOTE=ternapi]
[:D]
Şakası bir yana, balıkları bu konuda hafife alıyor olabiliriz. Bir kere gayet uzun süreli hafızaları olduğunu düşünüyorum.
Mesela medakalarım ve gambusyalarım 10-15 gün yalnız kalıyorlar. Sonrasında ben ya da eşim ponda yaklaştığımız anda hepsi resmen koşa koşa yanımıza geliyor. Normalde içgüdüsel olarak üstten kuş gölgesi geçse dibe kaçıp saklanan canlılar bunlar.
Betta yetiştirdiğim zamanlar ben tanka yaklaştığımda yem verirdim, eşim ise sadece bettayla oyun oynamak için yaklaşırdı. Ben tanka yaklaşınca balık hemen fıtı fıtı gelir, bir süre yem bekler ve yem yoksa totosunu dönüp toz olurdu[:)))] ama tanka yaklaşan eşimse hiç yem beklemeden parmak takip etmece oynamaya başlar ve dakikalarca oynarlardı, kıskanır sinir olurdum[:devil:][:D]
Niyet okumada da çok başarılılar. Elimde kepçeyle ponda yaklaşmalarımın neredeyse tamamı su piresi ya da larva falan vermek için oluyor ve bu şekilde her yaklaşmamda hepsi koşa koşa gelip toplaşırlar ama kırk yılda bir, elimde aynı kepçe, balıklardan birini yakalama niyetiyle yaklaştığımda resmen ışık hızıyla kayboluverir şapşikler[:devil:]
Bu durumu, zihin okuma dışında bir şeyle açıklamak bence çok zor[:cik:][/QUOTE]
Eski betam aynısıydı. Parmağımı soktuğum anda kendini sevdirmeye geliyordu. Yem vereceğim zaman da aynı şekilde. Bazen ailem verirdi yemi ben uyuya kaldığım için yemezdi o yemleri. Sabah akvaryum dibinden geri toplardım. Canlı yem bile verseler nazlanıyordu ben olmayınca. Hissediyor ve tanıyorlar gerçekten.
Betta Splendens Filtre
En sağlıklısı pipo ya da akıntı yapmayan askı filtreler. İç filtre her türlü akıntı yaratıyor maalesef uzun vadede yoruyor. Özellikle dumbo ear ya da half moon gibi bir varyete ise daha sıkıntı.
En sağlıklısı pipo ya da akıntı yapmayan askı filtreler. İç filtre her türlü akıntı yaratıyor maalesef uzun vadede yoruyor. Özellikle dumbo ear ya da half moon gibi bir varyete ise daha sıkıntı.
Hobi Odası Başlangıcı. :)
[QUOTE=Yorum yapmaz]
O dediğin ne yaa ben bişi anlamadım [:D][/QUOTE]
Işıkları kontrol etmek için.
[QUOTE=Yorum yapmaz]
O dediğin ne yaa ben bişi anlamadım [:D][/QUOTE]
Işıkları kontrol etmek için.
Esp Tabanlı Akıllı Işık Ve Sıcaklık Ölçüm Sistemi
[QUOTE=senkron24]Aman tanrim bu cok iyimis :)
Elimde esp32 s3 ve esp32-c6 var :)
valla acaip heveslen dim cok iyi bir projeymis emegine saglik hariak is cikartmisin mukkemel fikri olmus :)[:3][/QUOTE]
Aklınızda bulunsun; 12 V dişi-erkek jak alın. Işığı modüler yapın, böylece bir sıkıntı olduğu zaman lehimleri sökmekle uğraşmazsınız. Isıtıcı için de yine aynısı geçerli: 220 V dişi-erkek fiş ve priz alın. Isıtıcı bozulursa veya bir şey olursa, sistemi bozmadan sadece ısıtıcıyı değiştirirsiniz.
[QUOTE=senkron24]Aman tanrim bu cok iyimis :)
Elimde esp32 s3 ve esp32-c6 var :)
valla acaip heveslen dim cok iyi bir projeymis emegine saglik hariak is cikartmisin mukkemel fikri olmus :)[:3][/QUOTE]
Aklınızda bulunsun; 12 V dişi-erkek jak alın. Işığı modüler yapın, böylece bir sıkıntı olduğu zaman lehimleri sökmekle uğraşmazsınız. Isıtıcı için de yine aynısı geçerli: 220 V dişi-erkek fiş ve priz alın. Isıtıcı bozulursa veya bir şey olursa, sistemi bozmadan sadece ısıtıcıyı değiştirirsiniz.
Hobi Odası Başlangıcı. :)
[QUOTE=Yorum yapmaz][IMG]https://foto.akvaryum.com/fotolar/222067/251020261214521.jpg[/IMG]
Tekrar başlıyoruz bakalım yavaş ilerleyeceğim bu sefer[:)][/QUOTE]
Hadi inşallah. Ben yerinizde olsam bütün akvaryumlara teker teker esp32 bağlar, odaya da kamera koyup izlerdim. 15 litre akvaryumuma bile esp8266 bağladım :D
[QUOTE=Yorum yapmaz][IMG]https://foto.akvaryum.com/fotolar/222067/251020261214521.jpg[/IMG]
Tekrar başlıyoruz bakalım yavaş ilerleyeceğim bu sefer[:)][/QUOTE]
Hadi inşallah. Ben yerinizde olsam bütün akvaryumlara teker teker esp32 bağlar, odaya da kamera koyup izlerdim. 15 litre akvaryumuma bile esp8266 bağladım :D
Esp Tabanlı Akıllı Işık Ve Sıcaklık Ölçüm Sistemi
[QUOTE=Sfenks111]Helal olsun sana genç arkadaşım. Böyle bir konu içinde ayrıca teşekkür ederim. Ellerine sağlık.[/QUOTE]
Estağfurullah, bu arada eklemeyi unutmuşum; gün içi sıcaklık değişimlerini de görebiliyorum. 100 W ısıtıcı olmasına rağmen minimum sıcaklık dalgalanması ile idare ediyorum. 15 litre suyu, 2 saatte bir 10 dakika çalışarak 26 derecede tutuyor. 25.5'te çalışıp belli bir derecede kendini kapatıyor ve maksimum 26.5'i görüyor. İstesem bunu daha da kalibre edebilirim ama uğraşmayacağım. Beta için bu dalgalanmanın sorun olacağını sanmıyorum. Biraz işin fantezisine kaçtım diyebilirim. Tailscale ile subnet routing de ayarladım ev dışından da kullanabiliyorum. İstediğim yerden kontrol ediyorum. Bildirim işine de girecektim ama dedim yeter cılkını çıkarmayalım. [:)))]
[IMG]https://foto.akvaryum.com/fotolar/240101/251020260045211.jpg[/IMG]
Güncelleme2: Bir kaç defa prob hatası alınca yazılımı biraz güncelledim. Artık altta log kısmı mevcut. Yaşanan sorunları ve açıp kapanmaları oradan görebiliyorum.
[IMG]https://foto.akvaryum.com/fotolar/240101/251020261415441.jpg[/IMG][EDIT]NightWist,2026-10-25 14:15:57[/EDIT]
[QUOTE=Sfenks111]Helal olsun sana genç arkadaşım. Böyle bir konu içinde ayrıca teşekkür ederim. Ellerine sağlık.[/QUOTE]
Estağfurullah, bu arada eklemeyi unutmuşum; gün içi sıcaklık değişimlerini de görebiliyorum. 100 W ısıtıcı olmasına rağmen minimum sıcaklık dalgalanması ile idare ediyorum. 15 litre suyu, 2 saatte bir 10 dakika çalışarak 26 derecede tutuyor. 25.5'te çalışıp belli bir derecede kendini kapatıyor ve maksimum 26.5'i görüyor. İstesem bunu daha da kalibre edebilirim ama uğraşmayacağım. Beta için bu dalgalanmanın sorun olacağını sanmıyorum. Biraz işin fantezisine kaçtım diyebilirim. Tailscale ile subnet routing de ayarladım ev dışından da kullanabiliyorum. İstediğim yerden kontrol ediyorum. Bildirim işine de girecektim ama dedim yeter cılkını çıkarmayalım. [:)))]
[IMG]https://foto.akvaryum.com/fotolar/240101/251020260045211.jpg[/IMG]
Güncelleme2: Bir kaç defa prob hatası alınca yazılımı biraz güncelledim. Artık altta log kısmı mevcut. Yaşanan sorunları ve açıp kapanmaları oradan görebiliyorum.
[IMG]https://foto.akvaryum.com/fotolar/240101/251020261415441.jpg[/IMG][EDIT]NightWist,2026-10-25 14:15:57[/EDIT]
Merhaba Led Önerisi Arıyorum
[QUOTE=ziyasenkaya]SMD ledli armatürleri hiç denemedim, fakat bazı arkadaşlarım performansının düşük olduğunu söylüyor, hatta bir arkadaşım biyik bir deniz akvaryumuna 2 sıra aynı marka smd armatür almıştı. Ben eskiden beri power led kullanıyorum, ihtiyacıma göre kendim yapıyorum armatürümü. Fakat son bitkili akvaryumumda kenardaki anubias topluluğu yüksek ışık yüzünden çok yosun yapmıştı, sonrasında o bölgedeki aydınlatmayı düşürmek zorunda kaldım. Yani hem balıkların rengi parlasın hem anubias iyi olsun derseniz, ya anubiasları köşelere dibe yerleştirin, ya da armatürün o kısmına hafif bir filtre yapıp ışığı kısın. Aşağıdaki videoyu bir inceleyin derim.
[VID]https://www.youtube.com/watch?v=krP0sNrzs8A[/VID]
[/QUOTE]
Konudan bağımsız ama powerledi nasıl kendiniz işliyorsunuz? Mikro akvaryumum için lazım da.
[QUOTE=ziyasenkaya]SMD ledli armatürleri hiç denemedim, fakat bazı arkadaşlarım performansının düşük olduğunu söylüyor, hatta bir arkadaşım biyik bir deniz akvaryumuna 2 sıra aynı marka smd armatür almıştı. Ben eskiden beri power led kullanıyorum, ihtiyacıma göre kendim yapıyorum armatürümü. Fakat son bitkili akvaryumumda kenardaki anubias topluluğu yüksek ışık yüzünden çok yosun yapmıştı, sonrasında o bölgedeki aydınlatmayı düşürmek zorunda kaldım. Yani hem balıkların rengi parlasın hem anubias iyi olsun derseniz, ya anubiasları köşelere dibe yerleştirin, ya da armatürün o kısmına hafif bir filtre yapıp ışığı kısın. Aşağıdaki videoyu bir inceleyin derim.
[VID]https://www.youtube.com/watch?v=krP0sNrzs8A[/VID]
[/QUOTE]
Konudan bağımsız ama powerledi nasıl kendiniz işliyorsunuz? Mikro akvaryumum için lazım da.
Esp Tabanlı Akıllı Işık Ve Sıcaklık Ölçüm Sistemi
Güncelleme: Isıtıcı eklemesi yapıldı. Isıtıcı termostatına güvenmediğim için harici termostat olarak ESP8266'yı kullanmaya karar verdim. Isıtıcının fazla çalışmasını engellemek için güvenlik sistemleri ekledim. Örneğin, probu 10 saniye okuyamazsa ısıtıcıyı tamamen kapatıyor ve ışığı açıp kapatarak uyarı veriyor. Aynı şekilde birkaç önlem daha var. Ayrıca ısıtıcının dahili termostatı 28 dereceye ayarlı, istese de daha fazla yükselemez.
Bütün tesisatı elden geçirdim. Jumper kablolar ile modüler şekilde yaptım; bu sayede istediğim zaman değiştirmem çok kolay olacak. ESP'ye OTA (Over-The-Air) eklemesi yaptım, artık her güncelleme yapmak istediğimde plastik kutuyu açıp laptopu oraya taşımak zorunda değilim. İnternetten kodu doğrudan atabiliyorum. Bu sırada arayüzü de tamamen güncelledim.
Kodların bir kısmını kendim, bir kısmını yapay zekadan yardım alarak yazdım. Özellikle HTML ve arayüz kısmındaki animasyonları ona yaptırdım, çok başarılı bir şekilde yaptı. 192.168.1.100 üzerinden statik olarak hem bilgisayardan hem de telefondan erişilebiliyor. Telefon uygulaması da mevcut. APK paylaşmayacağım, kendiniz MIT App Inventor üzerinden yapabilirsiniz. Şart değil zaten; WebView şeklinde çalışıyor, yani web sitesini gösteriyor. Yerel ağdan çalıştırıyorum; böylece hem daha güvenli hem de stabil çalışıyor. Bu sefer kodu da paylaşacağım, kullanmak isteyen olursa kullanabilir.
[IMG]https://foto.akvaryum.com/fotolar/240101/241020261835101.jpg[/IMG][IMG]https://foto.akvaryum.com/fotolar/240101/241020261835091.jpg[/IMG][IMG]https://foto.akvaryum.com/fotolar/240101/241020261839551.jpg[/IMG][IMG]https://foto.akvaryum.com/fotolar/240101/241020261840051.jpg[/IMG][IMG]https://foto.akvaryum.com/fotolar/240101/241020261840111.jpg[/IMG]
[QUOTE]
/*
Akvaryum Kontrol - ESP8266 (NodeMCU)
- D1 (GPIO5) : Isıtıcı rölesi (low trigger: LOW=çeker)
- D2 (GPIO4) : Işık rölesi (low trigger: LOW=çeker)
- D3 (GPIO0) : DS18B20 data (4.7k pull-up - 3.3V)
Kütüphaneler (Library Manager): OneWire, DallasTemperature
Kart: NodeMCU 1.0 (ESP-12E Module)
*/
#include ESP8266WiFi.h
#include ESP8266WebServer.h
#include OneWire.h
#include DallasTemperature.h
#include EEPROM.h
#include time.h
#include ArduinoOTA.h
// ======================= AĞ AYARLARI =======================
const char* WIFI_SSID=internetisminiz";
const char* WIFI_PASS=sifreniz";
IPAddress ipAddr (192, 168, 1, 100);
IPAddress gwAddr (192, 168, 1, 1); // Modem adresi (farklıysa değiştir)
IPAddress maskAddr(255, 255, 255, 0);
IPAddress dns1Addr(192, 168, 1, 1);
IPAddress dns2Addr(8, 8, 8, 8);
const long GMT_OFFSET_SEC=3 * 3600; // Türkiye UTC+3
// ======================= PİNLER =======================
const uint8_t PIN_HEATER=5; // D1
const uint8_t PIN_LIGHT =4; // D2
const uint8_t PIN_TEMP =0; // D3
#define RELAY_ON LOW
#define RELAY_OFF HIGH
// RÖLE SÜRME YÖNTEMİ
// 0=Klasik: AÇIK - LOW, KAPALI - HIGH (pin hep OUTPUT).
// Bu röle modüllerinde 3.3V HIGH rölenin bırakması için yetmiyor (hep açık kalıyor).
// 1=Açık-uçlu: AÇIK - LOW, KAPALI - pin boşa alınır (INPUT). Röleler bırakır.
// Not: Bu modda röle girişi kapalıyken ~4V'ta kalıp hafif ses çıkarabiliyor.
#define RELAY_OPEN_DRAIN 1
void relayWrite(uint8_t pin, bool on) {
#if RELAY_OPEN_DRAIN
if (on) {
digitalWrite(pin, LOW);
pinMode(pin, OUTPUT);
} else {
pinMode(pin, INPUT);
}
#else
digitalWrite(pin, on ? LOW : HIGH);
pinMode(pin, OUTPUT);
#endif
}
// ======================= ISITICI / GÜVENLİK AYARLARI =======================
const float TARGET_MIN =20.0; // Web'den ayarlanabilecek en düşük hedef
const float TARGET_MAX =32.0; // Web'den ayarlanabilecek en yüksek hedef
const float TARGET_DEFAULT =25.0;
const float HYSTERESIS =0.3; // Aç ile kapat arasındaki fark
const float EARLY_OFF =0.2; // Isıtıcıyı hedefin bu kadar altında kapat (cam ısı vermeye devam eder)
// 100W/15L için 0.2. 25-50W ısıtıcıda 0.0 yapabilirsin.
const float ABS_MAX_C =33.5; // Bunun üstünde ne olursa olsun ısıtıcı kapalı + alarm
const unsigned long MAX_ON_MS =10UL * 60UL * 1000UL; // Tam güç eşdeğeri en fazla 10 dk ısıtma (POWER_PCT düşükse süre orantılı uzar)
const unsigned long COOLDOWN_MS =3UL * 60UL * 1000UL; // Sonra 3 dk zorunlu mola
// GÜÇ SINIRI: Web arayüzünden seçilir (100=tam güç, 50=yarım güç) ve kaydedilir.
// Yarım güçte ısıtıcı, 5 dk'lık dilimlerin yarısı kadar açık kalır (2,5 dk açık, 2,5 dk kapalı).
// Tam güçte röle en az tıklar. Aşağıdaki değer sadece ilk kurulumdaki varsayılandır.
const uint8_t POWER_PCT_DEFAULT=50;
const unsigned long PULSE_PERIOD_MS=300000UL;
const unsigned long SENSOR_TIMEOUT_MS=10000; // 10 sn geçerli okuma yoksa kapat
const unsigned long READ_INTERVAL_MS =3000;
// ======================= KALICI AYARLAR =======================
struct Settings {
uint32_t magic;
float targetC;
uint16_t lightOn; // gece yarısından beri dakika
uint16_t lightOff;
uint8_t powerPct; // 50 veya 100
};
const uint32_t MAGIC=0xA11C0DE1;
Settings cfg;
// ======================= DURUM =======================
ESP8266WebServer server(80);
OneWire oneWire(PIN_TEMP);
DallasTemperature sensors(&oneWire);
float tempC=NAN;
bool haveReading=false;
bool sensorOk=false;
unsigned long lastGoodMs=0, lastRequest=0;
bool converting=false;
uint8_t badCount=0;
bool heaterOn=false;
unsigned long heaterOnSince=0;
bool demand=false; // Termostat "ısıt" diyor mu (röle çıkışından bağımsız)
unsigned long demandSince=0;
bool cooling=false;
unsigned long coolStart=0;
const char* hstate=idle";
// --- Sıcaklık kaydı: 5 dakikada bir ortalama nokta, son 24 saat (RAM'de tutulur) ---
const unsigned long LOG_INTERVAL_MS=5UL * 60UL * 1000UL;
const uint16_t LOG_SIZE=288; // 288 x 5 dk=24 saat
const int16_t LOG_INVALID=-32768;
int16_t logTemp[LOG_SIZE]; // 0.1 °C cinsinden
uint8_t logHeat[LOG_SIZE]; // o aralıkta ısıtıcının açık kaldığı yüzde
uint16_t logHead=0, logCount=0;
float accSum=0;
uint16_t accN=0;
unsigned long accHeatMs=0, lastAccMs=0, logStartMs=0;
int lastLogMin=-1; // son kaydın günün kaçıncı dakikası olduğu (-1: saat yok)
bool lightOn=false; // Işığın mantıksal durumu (arayüzde görünen)
bool lightRelay=false; // Rölenin fiziksel durumu
bool blinkInvert=false; // Alarm yanıp sönmesi sırasında röleyi tersler
// --- Alarm uyarısı: alarm varken ışık 3 kez yanıp söner ---
const unsigned long ALARM_REPEAT_MS=120000UL; // Alarm sürerken uyarıyı tekrar etme aralığı (2 dk)
const unsigned long BLINK_STEP_MS =400UL; // Her yanma/sönme süresi
int8_t blinkStep=-1; // -1: yanıp sönme yok
unsigned long blinkNext=0, lastAlarmBlink=0;
bool prevAlarm=false;
// --- Soğuma izleme: su hedeften soğuk kalıp ısınmıyorsa (ısıtıcı bozuk ya da fişi çıkık) alarm ---
const float COLD_MARGIN =1.0; // Hedefin bu kadar altına inince izlemeye başla
const float COLD_MIN_RISE=0.3; // İzleme süresinde en az bu kadar ısınmalı
const unsigned long COLD_WATCH_MS=60UL * 60UL * 1000UL; // 60 dk
bool coldWatch=false, coldAlarm=false;
unsigned long coldSince=0;
float coldRef=0;
// --- WiFi ile kod yükleme (OTA) ---
const char* OTA_HOSTNAME=akvaryum";
const char* OTA_PASSWORD=akvaryum"; // Arduino IDE yüklerken bu şifreyi sorar
bool otaBusy=false, otaStarted=false;
uint8_t lightMode=0; // 0=otomatik, 1=elle açık, 2=elle kapalı (açılışta otomatiğe döner)
// ======================= WEB ARAYÜZÜ =======================
const char INDEX_HTML[] PROGMEM=R"rawliteral(
!DOCTYPE html
html lang=tr"head
meta charset=utf-8"
meta content=width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no,viewport-fit=cover"
meta content=#062b3a"
title🐠 Akvaryum Kontrol Merkezi/title
style
:root{--abyss:#062b3a;--tide:#13707f;--foam:#eaf7f5;--mute:#8dbdc3;--lamp:#f2c879;--coral:#ff8a5b;--ok:#7fe0d2;--bad:#ff6b7a}
*{box-sizing:border-box;-webkit-tap-highlight-color:transparent}
[hidden]{display:none!important}
body{margin:0;min-height:100vh;min-height:100dvh;background:var(--abyss);color:var(--foam);
font:16px/1.45 ui-rounded,"SF Pro Rounded",Nunito,system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;
padding:max(12px,env(safe-area-inset-top)) 14px max(28px,env(safe-area-inset-bottom))}
button,input{font-family:inherit}
button:focus-visible,input:focus-visible{outline:2px solid var(--lamp);outline-offset:2px}
main{max-width:460px;margin:0 auto}
h1{font-size:20px;margin:6px 4px 12px;display:flex;align-items:center;gap:8px}
.dot{width:10px;height:10px;border-radius:50%;background:var(--bad);margin-left:auto}
.dot.ok{background:var(--ok)}
.alarm{margin:0 0 12px;padding:12px 14px;border-radius:16px;background:rgba(255,107,122,.18);border:1px solid var(--bad);font-weight:600}
.tank{position:relative;overflow:hidden;isolation:isolate;border-radius:30px;padding:28px 20px 22px;text-align:center;
background:linear-gradient(180deg,#0e6070 0%,#08394a 100%);border:1px solid rgba(255,255,255,.1)}
.tank::before,.tank::after{content:"";position:absolute;left:0;right:0;z-index:-1;opacity:0;transition:opacity .8s;pointer-events:none}
.tank::before{top:0;bottom:0;background:radial-gradient(120% 70% at 50% -10%,rgba(242,200,121,.5),transparent 65%)}
.tank::after{bottom:0;height:50%;background:linear-gradient(0deg,rgba(255,138,91,.5),transparent)}
.tank.lit::before,.tank.heating::after{opacity:1}
.bub{position:absolute;bottom:-20px;width:var(--s);height:var(--s);border-radius:50%;border:1.5px solid rgba(255,255,255,.4);
opacity:0;animation:rise var(--d) linear infinite;animation-delay:var(--w)}
.tank.heating .bub{opacity:1}
@keyframes rise{to{transform:translateY(-320px)}}
@media (prefers-reduced-motion:reduce){.bub{animation:none}}
.temp{font-size:84px;font-weight:800;letter-spacing:-3px;line-height:1;font-variant-numeric:tabular-nums}
.temp small{font-size:28px;font-weight:600;letter-spacing:0;margin-left:4px;opacity:.7}
.sub{margin:8px 0 16px;color:var(--mute);font-size:15px}
.pill{display:inline-block;padding:7px 14px;border-radius:999px;font-weight:600;font-size:15px;background:rgba(255,255,255,.12)}
.pill.hot{background:var(--coral);color:#3a1204}
.pill.ok{background:rgba(127,224,210,.2);color:var(--ok)}
.pill.warn,.pill.on{background:var(--lamp);color:#3a2a05}
.pill.bad{background:var(--bad);color:#3b0710}
.pill.sm{font-size:13px;padding:4px 10px;margin-left:auto}
.panel{margin-top:14px;padding:16px;border-radius:22px;background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.08)}
h2{font-size:17px;margin:0 0 14px;display:flex;align-items:center;gap:8px}
.step{display:flex;align-items:center;justify-content:space-between;gap:10px}
.rb{width:64px;height:64px;border-radius:20px;border:0;background:rgba(255,255,255,.12);color:var(--foam);font-size:32px;font-weight:600}
.rb:active{background:rgba(255,255,255,.24)}
.tgt{font-size:44px;font-weight:800;font-variant-numeric:tabular-nums}
.tgt small{font-size:20px;font-weight:600;color:var(--mute);margin-left:2px}
.chips{display:flex;gap:8px;margin:14px 0}
.chip{flex:1;min-height:44px;border-radius:14px;border:1px solid rgba(255,255,255,.2);background:transparent;color:var(--foam);font-size:16px;font-weight:600}
.chip.on{background:var(--ok);color:#06302b;border-color:var(--ok)}
.go{width:100%;min-height:52px;border:0;border-radius:16px;font-size:17px;font-weight:700}
.go.mint{background:var(--ok);color:#06302b}
.go.amber{background:var(--lamp);color:#3a2a05}
.go:disabled{opacity:.35}
.seg{display:grid;grid-template-columns:repeat(3,1fr);gap:6px;padding:5px;border-radius:18px;background:rgba(0,0,0,.25)}
.seg button{min-height:46px;border:0;border-radius:13px;background:transparent;color:var(--foam);font-size:14.5px;font-weight:600}
.seg button.on{background:var(--lamp);color:#3a2a05}
.times{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin:14px 0}
.times label{display:flex;flex-direction:column;gap:6px;font-size:14px;color:var(--mute)}
.times input{width:100%;min-height:52px;border-radius:14px;border:1px solid rgba(255,255,255,.2);background:rgba(0,0,0,.25);
color:var(--foam);font-size:20px;font-weight:600;padding:0 12px;color-scheme:dark}
.seg.mint button.on{background:var(--ok);color:#06302b}
.seg.two{grid-template-columns:1fr 1fr}
.seg.hot button.on{background:var(--coral);color:#3a1204}
.chart{margin-top:12px}
.chart svg{width:100%;height:auto;display:block;touch-action:pan-y;user-select:none;-webkit-user-select:none}
.gr{stroke:rgba(255,255,255,.1);stroke-width:1}
.ax{fill:var(--mute);font-size:10px}
.cl{fill:none;stroke:var(--ok);stroke-width:2.5;stroke-linejoin:round;stroke-linecap:round}
.tl{stroke:var(--lamp);stroke-width:1.2;stroke-dasharray:4 4;opacity:.85}
.hb{fill:var(--coral);opacity:.55}
.cur{stroke:var(--foam);stroke-width:1;opacity:.6}
.cc{fill:var(--ok);stroke:var(--abyss);stroke-width:2}
.empty{padding:34px 8px;text-align:center;color:var(--mute)}
.info{min-height:22px;margin-top:6px;font-size:14px;text-align:center;font-variant-numeric:tabular-nums}
.legend{display:flex;gap:14px;justify-content:center;margin-top:6px;font-size:12px;color:var(--mute)}
.legend span::before{content:"";display:inline-block;width:12px;height:4px;border-radius:2px;margin-right:5px;vertical-align:middle}
.l1::before{background:var(--ok)}.l2::before{background:var(--lamp)}.l3::before{background:var(--coral)}
.stats{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-top:12px}
.stats div{padding:10px 12px;border-radius:14px;background:rgba(0,0,0,.2);display:flex;flex-direction:column}
.stats small{font-size:12px;color:var(--mute)}
.stats b{font-size:20px;font-variant-numeric:tabular-nums}
.hint{margin:12px 0 0;font-size:12px;color:var(--mute)}
footer{display:flex;justify-content:space-between;margin:18px 6px 0;font-size:13px;color:var(--mute)}
.toast{position:fixed;left:50%;bottom:max(22px,env(safe-area-inset-bottom));transform:translate(-50%,30px);padding:12px 18px;
border-radius:14px;background:var(--foam);color:#06302b;font-weight:600;opacity:0;pointer-events:none;transition:.25s}
.toast.show{opacity:1;transform:translate(-50%,0)}
/style/head
bodymain
h1🐠 Akvaryum Kontrol Merkezi i/i/h1
div hidden/div
section
i/i
i/i
i/i
i/i
i/i
i/i
i/i
divspan--.-/spansmall°C/small/div
divHedef --/div
divBağlanıyor…/div
/section
section
h2📈 Sıcaklık geçmişi/h2
div
button data-n=72"6 saat/button
button data-n=144"12 saat/button
button data-n=288"24 saat/button
/div
divdivYükleniyor…/div/div
div/div
divspanSıcaklık/spanspanHedef/spanspanIsıtıcı açık/span/div
div hidden
divsmallEn düşük/smallb--/b/div
divsmallOrtalama/smallb--/b/div
divsmallEn yüksek/smallb--/b/div
divsmall🔥 Isıtıcı açık/smallb--/b/div
/div
pHer 5 dakikada bir nokta eklenir. Cihaz yeniden başlayınca geçmiş sıfırlanır./p
/section
section
h2🎯 Hedef sıcaklık/h2
div
button aria-label=Azalt"−/button
divspan--.-/spansmall°C/small/div
button aria-label=Artır"+/button
/div
div/div
button disabledHedefi kaydet/button
/section
section
h2⚡ Isıtıcı gücü/h2
div
button🔥 Tam güç/button
button🌿 Yarım güç/button
/div
pYarım güçte ısıtıcı 2,5 dk açık, 2,5 dk kapalı çalışır. Küçük akvaryumda sıcaklık daha dengeli kalır ama ısınma yavaşlar. Tam güçte röle daha az tıklar./p
/section
section
h2💡 Işık span…/span/h2
div
button🤖 Otomatik/button
button☀️ Açık/button
button🌙 Kapalı/button
/div
div
label🌅 Açılışinput type=time" oninput=dl()"/label
label🌇 Kapanışinput type=time" oninput=dl()"/label
/div
button disabledSaatleri kaydet/button
/section
footerspan🕒 --/spanspan📶 --/spanspan⏱ --/span/footer
div/div
/main
script
const $=id=document.getElementById(id);
const clamp=(v,a,b)=Math.min(b,Math.max(a,v));
let S=null,pt=null,dirtyT=false,dirtyL=false;
async function api(p){const r=await fetch(p,{cache:'no-store'});if(!r.ok)throw new Error(await r.text());return r.json()}
function toast(m){const t=$('toast');t.textContent=m;t.classList.add('show');clearTimeout(toast.h);toast.h=setTimeout(()=t.classList.remove('show'),1800)}
function up(x){const d=Math.floor(x/86400),h=Math.floor(x%86400/3600),m=Math.floor(x%3600/60);return d?d+' g '+h+' sa':h?h+' sa '+m+' dk':m+' dk'}
function showTarget(){
$('tgt').textContent=pt.toFixed(1);
$('saveT').disabled=!dirtyT;
document.querySelectorAll('.chip').forEach(b=b.classList.toggle('on',Math.abs(+b.dataset.v-pt)0.05));
}
function adj(d){if(!S)return;pt=clamp(Math.round((pt+d)*10)/10,S.min,S.max);dirtyT=true;showTarget()}
function pick(v){if(!S)return;pt=clamp(v,S.min,S.max);dirtyT=true;showTarget()}
function dl(){dirtyL=true;$('saveL').disabled=false}
async function saveT(){
try{S=await api('/api/temp?t=+pt);dirtyT=false;render();toast('✅ Hedef kaydedildi')}
catch(e){toast('⚠️ Hedef kaydedilemedi')}
}
async function setMode(m){
try{S=await api('/api/light?mode=+m);render()}
catch(e){toast('⚠️ Işık modu değişmedi')}
}
async function setPower(p){
try{S=await api('/api/power?p=+p);render();toast(p===100?'✅ Tam güç seçildi':'✅ Yarım güç seçildi')}
catch(e){toast('⚠️ Güç ayarı değişmedi')}
}
async function saveL(){
try{S=await api('/api/light?on=+encodeURIComponent($('tOn').value)+'&off=+encodeURIComponent($('tOff').value));
dirtyL=false;render();toast('✅ Işık saatleri kaydedildi')}
catch(e){toast('⚠️ Saatler kaydedilemedi, iki saati de seç')}
}
function render(){
const s=S;
$('temp').textContent=s.temp===null?'--.-':s.temp.toFixed(1);
$('sub').textContent=Hedef '+s.target.toFixed(1)+' °C';
const A={sensor:'⚠️ Sıcaklık probu okunamıyor. Güvenlik için ısıtıcı kapalı tutuluyor. Prob kablosunu ve 4.7k direnci kontrol et.',
over:'🚨 Su çok sıcak! Isıtıcıyı fişten çek ve rölenin yapışıp yapışmadığını kontrol et.',
cold:'🥶 Su hedeften soğuk ve uzun süredir ısınmıyor. Isıtıcı çalışmıyor olabilir, fişini ve bağlantıları kontrol et.'};
$('alarm').hidden=!A[s.alarm];$('alarm').textContent=A[s.alarm]||'';
const H={heating:['🔥 Isıtıyor','hot],idle:['😌 Sıcaklık yerinde','ok],sensor:['⛔ Prob yok, ısıtıcı kapalı','bad],
cooldown:['🧊 Güvenlik molası','warn],over:['🚨 Aşırı sıcak, ısıtıcı kapalı','bad]};
const h=H[s.hstate]||H.idle;
$('hpill').textContent=h[0];$('hpill').className=pill '+h[1];
$('tank').classList.toggle('heating',s.heater);
$('tank').classList.toggle('lit',s.light);
if(!dirtyT){pt=s.target}
showTarget();
let lp;
if(s.mode===0&&!s.timeOk)lp=⏳ Saat bekleniyor';
else lp=(s.light?'💡 Açık':'🌙 Kapalı')+(s.mode===0?' (otomatik)':' (elle)');
$('lpill').textContent=lp;$('lpill').className=pill sm'+(s.light?' on':'');
document.querySelectorAll('#seg button').forEach((b,i)=b.classList.toggle('on',i===s.mode));
if(!dirtyL){$('tOn').value=s.on;$('tOff').value=s.off}
$('saveL').disabled=!dirtyL;
$('fTime').textContent=🕒 '+s.now;
$('fWifi').textContent=📶 '+s.rssi+' dBm';
$('fUp').textContent=⏱ '+up(s.up);
document.querySelectorAll('#pw button').forEach((b,i)=b.classList.toggle('on',(i===0?100:50)===s.power));
if(LG&&render.tg!==s.target){render.tg=s.target;drawLog()}
}
async function refresh(){
try{S=await api('/api/status');$('dot').classList.add('ok');render()}
catch(e){$('dot').classList.remove('ok')}
}
let LG=null,rng=288,CH=null;
const f1=v=v.toFixed(1),pad2=n=String(n).padStart(2,'0');
function hm(m){m=((m%1440)+1440)%1440;return pad2(Math.floor(m/60))+':'+pad2(m%60)}
async function loadLog(){try{LG=await api('/api/log');drawLog()}catch(e){}}
function setRng(n){rng=n;document.querySelectorAll('#rng button').forEach(b=b.classList.toggle('on',+b.dataset.n===n));drawLog()}
function drawLog(){
if(!LG)return;
const n=Math.min(LG.t.length,rng),box=$('chart'),st=$('stats');
const T=LG.t.slice(LG.t.length-n),Hh=LG.h.slice(LG.h.length-n);
const vals=T.filter(v=v!==null).map(v=v/10);
if(!vals.length){box.innerHTML=divHenüz kayıt yok. İlk nokta 5 dakika içinde eklenir./div';st.hidden=true;CH=null;$('lgInfo').textContent=';return}
st.hidden=false;
const W=340,HT=180,L=36,R=8,TP=10,B=24,pw=W-L-R,ph=HT-TP-B,tg=S?S.target:null;
let lo=Math.min(...vals,tg===null?99:tg),hi=Math.max(...vals,tg===null?-99:tg);
lo=Math.floor((lo-.3)*2)/2;hi=Math.ceil((hi+.3)*2)/2;
if(hi-lo2){const c=(hi+lo)/2;lo=c-1;hi=c+1}
const X=i=L+(n1?i*pw/(n-1):pw/2),Y=v=TP+ph*(1-(v-lo)/(hi-lo));
let g=';
for(let k=0;k=4;k++){const v=lo+k*(hi-lo)/4,y=Y(v);
g+=`line x1=${L}" x2=${W-R}" y1=${f1(y)}" y2=${f1(y)}"/text x=${L-5}" y=${f1(y+3.5)}" text-anchor=end"${f1(v)}/text`}
const bw=Math.max(1,pw/n*.8);
Hh.forEach((p,i)={if(p0){const h=p/100*24;g+=`rect x=${f1(X(i)-bw/2)}" y=${f1(TP+ph-h)}" width=${f1(bw)}" height=${f1(h)}"/`}});
if(tg!==null)g+=`line x1=${L}" x2=${W-R}" y1=${f1(Y(tg))}" y2=${f1(Y(tg))}"/`;
let d=',pen=false;
T.forEach((v,i)={if(v===null){pen=false;return}d+=(pen?'L':'M')+f1(X(i))+' '+f1(Y(v/10))+(pen?'':' h.01');pen=true});
for(let k=0;k4;k++){
const i=Math.round(k*(n-1)/3),ago=(n-1-i)*LG.step;
const lab=LG.end=0?hm(LG.end-ago):(ago?'-'+Math.round(ago/60)+' sa':'son');
g+=`text x=${f1(X(i))}" y=${HT-6}" text-anchor=${k===0?'start':k===3?'end':'middle'}"${lab}/text`}
box.innerHTML=`svg viewBox=0 0 ${W} ${HT}" role=img" aria-label=Sıcaklık grafiği"${g}path d=${d}"/`+
`line y1=${TP}" y2=${TP+ph}" x1=0" x2=0"/circle r=4"//svg`;
CH={n,T,Hh,X,Y,W,L,pw};
const sv=$('svg');sv.addEventListener('pointerdown',probe);sv.addEventListener('pointermove',probe);
const mn=Math.min(...vals),mx=Math.max(...vals),av=vals.reduce((a,b)=a+b,0)/vals.length;
const mins=Hh.reduce((a,p)=a+p,0)/100*LG.step;
$('sMin').textContent=f1(mn)+'°';$('sAvg').textContent=f1(av)+'°';$('sMax').textContent=f1(mx)+'°';
$('sHeat').textContent=mins=60?Math.floor(mins/60)+' sa '+Math.round(mins%60)+' dk':Math.round(mins)+' dk';
$('lgInfo').textContent=Grafiğe dokun, o andaki değeri gör';
}
function probe(e){
if(!CH)return;
const r=$('svg').getBoundingClientRect(),x=(e.clientX-r.left)/r.width*CH.W;
let i=CH.n1?Math.round((x-CH.L)/CH.pw*(CH.n-1)):0;i=Math.max(0,Math.min(CH.n-1,i));
const v=CH.T[i],cur=$('cur'),cc=$('cc'),px=CH.X(i),ago=(CH.n-1-i)*LG.step;
const when=LG.end=0?hm(LG.end-ago):(ago?(ago=60?Math.round(ago/60)+' sa önce':ago+' dk önce'):'en son');
cur.setAttribute('x1',px);cur.setAttribute('x2',px);cur.style.display=';
if(v===null){cc.style.display=none';$('lgInfo').textContent=🕒 '+when+' veri yok';return}
cc.setAttribute('cx',px);cc.setAttribute('cy',CH.Y(v/10));cc.style.display=';
$('lgInfo').textContent=🕒 '+when+' 🌡️ '+f1(v/10)+' °C 🔥 %'+CH.Hh[i];
}
[24,26,28,30].forEach(v={
const b=document.createElement('button');b.className=chip';b.dataset.v=v;b.textContent=v+'°';
b.onclick=()=pick(v);$('chips').appendChild(b);
});
refresh();loadLog();
setInterval(()={if(!document.hidden)refresh()},3000);
setInterval(()={if(!document.hidden)loadLog()},60000);
document.addEventListener('visibilitychange',()={if(!document.hidden)refresh()});
/script/body/html
)rawliteral";
// ======================= YARDIMCI FONKSİYONLAR =======================
void saveSettings() {
EEPROM.put(0, cfg);
EEPROM.commit();
}
void loadSettings() {
EEPROM.get(0, cfg);
if (cfg.magic != MAGIC || isnan(cfg.targetC) || cfg.targetC TARGET_MIN || cfg.targetC TARGET_MAX ||
cfg.lightOn=1440 || cfg.lightOff=1440) {
cfg.magic =MAGIC;
cfg.targetC =TARGET_DEFAULT;
cfg.lightOn =10 * 60; // 10:00
cfg.lightOff=20 * 60; // 20:00
cfg.powerPct=POWER_PCT_DEFAULT;
saveSettings();
}
// Eski kayıtta güç alanı yoktu: geçersizse varsayılana al (hedef ve ışık saatleri korunur)
if (cfg.powerPct != 50 && cfg.powerPct != 100) {
cfg.powerPct=POWER_PCT_DEFAULT;
saveSettings();
}
}
bool timeOk() { return time(nullptr) 1700000000; }
int nowMinutes() {
time_t t=time(nullptr);
struct tm* lt=localtime(&t);
return lt-tm_hour * 60 + lt-tm_min;
}
String nowString() {
if (!timeOk()) return "--:--:--";
time_t t=time(nullptr);
struct tm* lt=localtime(&t);
char b[9];
snprintf(b, sizeof(b), "%02d:%02d:%02d", lt-tm_hour, lt-tm_min, lt-tm_sec);
return String(b);
}
String fmtHM(int m) {
char b[6];
snprintf(b, sizeof(b), "%02d:%02d", m / 60, m % 60);
return String(b);
}
int parseHM(const String& s) {
int c=s.indexOf(':');
if (c 1) return -1;
int h=s.substring(0, c).toInt();
int m=s.substring(c + 1).toInt();
if (h 0 || h 23 || m 0 || m 59) return -1;
return h * 60 + m;
}
// ======================= SICAKLIK OKUMA (bloklamadan) =======================
void updateTemperature() {
unsigned long now=millis();
if (!converting) {
if (now - lastRequest=READ_INTERVAL_MS) {
sensors.requestTemperatures();
converting=true;
lastRequest=now;
}
} else if (now - lastRequest=800) {
converting=false;
float t=sensors.getTempCByIndex(0);
if (t == DEVICE_DISCONNECTED_C || t == 85.0 || t -20.0 || t 60.0) {
if (++badCount=10) { // ~30 sn hatalı okuma: probu yeniden tara
sensors.begin();
sensors.setWaitForConversion(false);
badCount=0;
}
} else {
tempC=t;
accSum += t;
accN++;
haveReading=true;
lastGoodMs=now;
badCount=0;
}
}
}
// ======================= ISITICI KONTROLÜ =======================
void setHeater(bool on) {
if (on == heaterOn) return;
heaterOn=on;
relayWrite(PIN_HEATER, on);
if (on) heaterOnSince=millis();
Serial.printf("Isitici %s (%.1f C, hedef %.1f)\n", on ? "ACIK" : "KAPALI", tempC, cfg.targetC);
}
void controlHeater() {
unsigned long now=millis();
sensorOk=haveReading && (now - lastGoodMs SENSOR_TIMEOUT_MS);
if (otaBusy) { // Kod yüklenirken ısıtıcı kapalı kalır
setHeater(false);
hstate=idle";
return;
}
if (cooling && now - coolStart=COOLDOWN_MS) {
cooling=false;
demandSince=now; // Mola bitti, süre sayacı baştan başlar
}
hstate=idle";
bool want=false;
if (!sensorOk) {
demand=false; // Prob yoksa ısıtıcı kapalı
hstate=sensor";
} else if (tempC=ABS_MAX_C) {
demand=false; // Mutlak üst sınır
hstate=over";
} else {
// 1) Termostat: ısıtma talebi var mı (histerezisli)
float offAt=cfg.targetC - EARLY_OFF;
bool d=demand;
if (tempC=offAt) d=false;
else if (tempC=offAt - HYSTERESIS) d=true;
if (d && !demand) demandSince=now;
demand=d;
// 2) Süre limiti: talep, tam güç eşdeğeri MAX_ON_MS'yi aşarsa zorunlu mola
unsigned long pct=constrain(cfg.powerPct, 10, 100);
if (demand && !cooling && now - demandSince=MAX_ON_MS * 100UL / pct) {
cooling=true;
coolStart=now;
}
// 3) Çıkış: talep varsa ve mola yoksa, güç sınırına göre darbeli aç
if (demand) {
if (cooling) {
hstate=cooldown";
} else {
hstate=heating";
bool pulseOn=true;
if (pct 100) pulseOn=((now - demandSince) % PULSE_PERIOD_MS) (PULSE_PERIOD_MS * pct / 100);
want=pulseOn;
}
}
}
setHeater(want);
}
// ======================= IŞIK KONTROLÜ =======================
bool scheduleSaysOn() {
if (!timeOk()) return false;
int m=nowMinutes();
int a=cfg.lightOn, b=cfg.lightOff;
if (a == b) return false;
if (a b) return m=a && m b;
return m=a || m b; // Gece yarısını aşan program (örn. 18:00 - 02:00)
}
void controlLight() {
bool want;
if (lightMode == 1) want=true;
else if (lightMode == 2) want=false;
else want=scheduleSaysOn();
if (want != lightOn) {
lightOn=want;
Serial.printf("Isik %s\n", want ? "ACIK" : "KAPALI");
}
bool phys=(want != blinkInvert); // Alarm yanıp sönerken röle tersine çevrilir
if (phys != lightRelay) {
lightRelay=phys;
relayWrite(PIN_LIGHT, phys);
}
}
// ======================= SICAKLIK KAYDI =======================
void logTick() {
unsigned long now=millis();
if (heaterOn) accHeatMs += now - lastAccMs;
lastAccMs=now;
if (now - logStartMs=LOG_INTERVAL_MS) {
logStartMs += LOG_INTERVAL_MS;
int16_t v=accN ? (int16_t)roundf(accSum / accN * 10.0f) : LOG_INVALID;
unsigned long pct=accHeatMs * 100UL / LOG_INTERVAL_MS;
if (pct 100) pct=100;
logTemp[logHead]=v;
logHeat[logHead]=(uint8_t)pct;
logHead=(logHead + 1) % LOG_SIZE;
if (logCount LOG_SIZE) logCount++;
lastLogMin=timeOk() ? nowMinutes() : -1;
accSum=0;
accN=0;
accHeatMs=0;
}
}
void sendLog() {
String j;
j.reserve(3200);
uint16_t start=(logHead + LOG_SIZE - logCount) % LOG_SIZE;
j += F("{\"step\":");
j += (int)(LOG_INTERVAL_MS / 60000UL);
j += F(",\"end\":");
j += lastLogMin;
j += F(",\"t\":[");
for (uint16_t k=0; k logCount; k++) {
int16_t v=logTemp[(start + k) % LOG_SIZE];
if (k) j += ',';
if (v == LOG_INVALID) j += F("null");
else j += (int)v;
}
j += F(],\"h\":[");
for (uint16_t k=0; k logCount; k++) {
if (k) j += ',';
j += (int)logHeat[(start + k) % LOG_SIZE];
}
j += F(]}");
server.sendHeader("Cache-Control", "no-store");
server.send(200, "application/json", j);
}
// ======================= WEB SUNUCU =======================
const char* alarmCode() {
if (sensorOk && tempC=ABS_MAX_C) return "over";
if (!sensorOk && millis() 20000) return "sensor";
if (coldAlarm) return "cold";
return "";
}
// Alarm varken ışığı 3 kez ters çevirip eski haline getirir (bloklamadan).
// Alarm başlar başlamaz hemen bir kez, sonra ALARM_REPEAT_MS aralığıyla tekrar eder.
void alarmBlinkTick() {
unsigned long now=millis();
bool alarm=alarmCode()[0] != '\0';
if (alarm && !prevAlarm) lastAlarmBlink=now - ALARM_REPEAT_MS; // yeni alarm: hemen uyar
prevAlarm=alarm;
if (blinkStep 0) {
if (!alarm || now - lastAlarmBlink ALARM_REPEAT_MS) return;
blinkStep=0;
blinkNext=now;
lastAlarmBlink=now;
}
if ((long)(now - blinkNext) 0) return;
if (blinkStep=6) { // 3 yanıp sönme bitti
blinkInvert=false;
blinkStep=-1;
return;
}
blinkInvert=(blinkStep % 2 == 0); // çift adım: ters çevir, tek adım: eski haline getir
blinkStep++;
blinkNext=now + BLINK_STEP_MS;
}
// Su hedeften soğukta kalıp 60 dk boyunca ısınmıyorsa "cold" alarmı verir.
void coldWatchTick() {
if (!sensorOk) return;
unsigned long now=millis();
if (tempC=cfg.targetC - COLD_MARGIN) {
if (!coldWatch) {
coldWatch=true;
coldSince=now;
coldRef=tempC;
} else if (tempC=coldRef + COLD_MIN_RISE) { // Isınıyor: pencereyi yenile
coldRef=tempC;
coldSince=now;
coldAlarm=false;
} else if (now - coldSince=COLD_WATCH_MS) {
coldAlarm=true;
}
} else {
coldWatch=false;
coldAlarm=false;
}
}
// ======================= WIFI İLE KOD YÜKLEME (OTA) =======================
void startOta() {
ArduinoOTA.setHostname(OTA_HOSTNAME);
ArduinoOTA.setPassword(OTA_PASSWORD);
ArduinoOTA.onStart([]() {
otaBusy=true;
setHeater(false); // Yükleme sırasında ısıtıcı kapalı
Serial.println("OTA basladi: isitici kapatildi");
});
ArduinoOTA.onEnd([]() { Serial.println("OTA bitti"); });
ArduinoOTA.onError([](ota_error_t e) {
otaBusy=false;
Serial.printf("OTA hata: %u\n", (unsigned)e);
});
ArduinoOTA.begin();
otaStarted=true;
Serial.println("OTA hazir");
}
String statusJson() {
String j;
j.reserve(420);
j += F("{\"temp\":"); j += sensorOk ? String(tempC, 1) : String("null");
j += F(",\"target\":"); j += String(cfg.targetC, 1);
j += F(",\"min\":"); j += String(TARGET_MIN, 1);
j += F(",\"max\":"); j += String(TARGET_MAX, 1);
j += F(",\"power\":"); j += (int)cfg.powerPct;
j += F(",\"heater\":"); j += heaterOn ? "true" : "false";
j += F(",\"hstate\":\""); j += hstate; j += '"';
j += F(",\"light\":"); j += lightOn ? "true" : "false";
j += F(",\"mode\":"); j += (int)lightMode;
j += F(",\"on\":\""); j += fmtHM(cfg.lightOn); j += '"';
j += F(",\"off\":\""); j += fmtHM(cfg.lightOff); j += '"';
j += F(",\"timeOk\":"); j += timeOk() ? "true" : "false";
j += F(",\"now\":\""); j += nowString(); j += '"';
j += F(",\"alarm\":\""); j += alarmCode(); j += '"';
j += F(",\"rssi\":"); j += (long)WiFi.RSSI();
j += F(",\"up\":"); j += (unsigned long)(millis() / 1000);
j += '}';
return j;
}
void sendStatus() {
server.sendHeader("Cache-Control", "no-store");
server.send(200, "application/json", statusJson());
}
void handleRoot() {
server.send_P(200, PSTR("text/html; charset=utf-8"), INDEX_HTML);
}
void handleTemp() {
if (!server.hasArg("t")) { server.send(400, "text/plain", "t eksik"); return; }
float t=server.arg("t").toFloat();
if (t TARGET_MIN || t TARGET_MAX) { server.send(400, "text/plain", "aralik disi"); return; }
cfg.targetC=roundf(t * 10.0f) / 10.0f;
saveSettings();
Serial.printf("Yeni hedef: %.1f C\n", cfg.targetC);
sendStatus();
}
void handlePower() {
if (!server.hasArg("p")) { server.send(400, "text/plain", "p eksik"); return; }
int p=server.arg("p").toInt();
if (p != 50 && p != 100) { server.send(400, "text/plain", "50 veya 100"); return; }
cfg.powerPct=p;
saveSettings();
Serial.printf("Isitici gucu: %%%d\n", p);
sendStatus();
}
void handleLight() {
if (server.hasArg("mode")) {
int m=server.arg("mode").toInt();
if (m 0 || m 2) { server.send(400, "text/plain", "mod hatali"); return; }
lightMode=m;
}
if (server.hasArg("on") || server.hasArg("off")) {
int a=parseHM(server.arg("on"));
int b=parseHM(server.arg("off"));
if (a 0 || b 0) { server.send(400, "text/plain", "saat hatali"); return; }
cfg.lightOn=a;
cfg.lightOff=b;
saveSettings();
}
controlLight();
sendStatus();
}
// ======================= WIFI =======================
void connectWifi() {
WiFi.mode(WIFI_STA);
WiFi.config(ipAddr, gwAddr, maskAddr, dns1Addr, dns2Addr);
WiFi.begin(WIFI_SSID, WIFI_PASS);
}
void checkWifi() {
static unsigned long lastTry=0;
if (WiFi.status() == WL_CONNECTED) return;
if (millis() - lastTry 20000) {
lastTry=millis();
WiFi.disconnect();
connectWifi();
}
}
// ======================= SETUP / LOOP =======================
void setup() {
// Röleleri en başta KAPALI yap
relayWrite(PIN_HEATER, false);
relayWrite(PIN_LIGHT, false);
Serial.begin(115200);
Serial.println();
EEPROM.begin(64);
loadSettings();
sensors.begin();
sensors.setWaitForConversion(false);
lastRequest=millis() - READ_INTERVAL_MS;
logStartMs=lastAccMs=millis();
WiFi.persistent(false);
WiFi.setAutoReconnect(true);
WiFi.setSleepMode(WIFI_NONE_SLEEP);
connectWifi();
unsigned long t0=millis();
while (WiFi.status() != WL_CONNECTED && millis() - t0 20000) {
delay(250);
}
if (WiFi.status() == WL_CONNECTED) {
Serial.print("Baglandi: ");
Serial.println(WiFi.localIP());
} else {
Serial.println("WiFi baglanamadi, arka planda denenecek");
}
configTime(GMT_OFFSET_SEC, 0, "pool.ntp.org", "time.google.com", "time.cloudflare.com");
server.on("/", handleRoot);
server.on("/api/status", sendStatus);
server.on("/api/temp", handleTemp);
server.on("/api/light", handleLight);
server.on("/api/power", handlePower);
server.on("/api/log", sendLog);
server.on("/favicon.ico", []() { server.send(204); });
server.onNotFound([]() { server.send(404, "text/plain", "Bulunamadi"); });
server.begin();
}
void loop() {
ArduinoOTA.handle();
if (!otaStarted && WiFi.status() == WL_CONNECTED) startOta();
server.handleClient();
updateTemperature();
controlHeater(); // WiFi olmasa bile çalışır
alarmBlinkTick();
controlLight();
logTick();
coldWatchTick();
checkWifi();
}
[/QUOTE][EDIT]NightWist,2026-10-24 18:40:22[/EDIT]
Güncelleme: Isıtıcı eklemesi yapıldı. Isıtıcı termostatına güvenmediğim için harici termostat olarak ESP8266'yı kullanmaya karar verdim. Isıtıcının fazla çalışmasını engellemek için güvenlik sistemleri ekledim. Örneğin, probu 10 saniye okuyamazsa ısıtıcıyı tamamen kapatıyor ve ışığı açıp kapatarak uyarı veriyor. Aynı şekilde birkaç önlem daha var. Ayrıca ısıtıcının dahili termostatı 28 dereceye ayarlı, istese de daha fazla yükselemez.
Bütün tesisatı elden geçirdim. Jumper kablolar ile modüler şekilde yaptım; bu sayede istediğim zaman değiştirmem çok kolay olacak. ESP'ye OTA (Over-The-Air) eklemesi yaptım, artık her güncelleme yapmak istediğimde plastik kutuyu açıp laptopu oraya taşımak zorunda değilim. İnternetten kodu doğrudan atabiliyorum. Bu sırada arayüzü de tamamen güncelledim.
Kodların bir kısmını kendim, bir kısmını yapay zekadan yardım alarak yazdım. Özellikle HTML ve arayüz kısmındaki animasyonları ona yaptırdım, çok başarılı bir şekilde yaptı. 192.168.1.100 üzerinden statik olarak hem bilgisayardan hem de telefondan erişilebiliyor. Telefon uygulaması da mevcut. APK paylaşmayacağım, kendiniz MIT App Inventor üzerinden yapabilirsiniz. Şart değil zaten; WebView şeklinde çalışıyor, yani web sitesini gösteriyor. Yerel ağdan çalıştırıyorum; böylece hem daha güvenli hem de stabil çalışıyor. Bu sefer kodu da paylaşacağım, kullanmak isteyen olursa kullanabilir.
[IMG]https://foto.akvaryum.com/fotolar/240101/241020261835101.jpg[/IMG][IMG]https://foto.akvaryum.com/fotolar/240101/241020261835091.jpg[/IMG][IMG]https://foto.akvaryum.com/fotolar/240101/241020261839551.jpg[/IMG][IMG]https://foto.akvaryum.com/fotolar/240101/241020261840051.jpg[/IMG][IMG]https://foto.akvaryum.com/fotolar/240101/241020261840111.jpg[/IMG]
[QUOTE]
/*
Akvaryum Kontrol - ESP8266 (NodeMCU)
- D1 (GPIO5) : Isıtıcı rölesi (low trigger: LOW=çeker)
- D2 (GPIO4) : Işık rölesi (low trigger: LOW=çeker)
- D3 (GPIO0) : DS18B20 data (4.7k pull-up - 3.3V)
Kütüphaneler (Library Manager): OneWire, DallasTemperature
Kart: NodeMCU 1.0 (ESP-12E Module)
*/
#include ESP8266WiFi.h
#include ESP8266WebServer.h
#include OneWire.h
#include DallasTemperature.h
#include EEPROM.h
#include time.h
#include ArduinoOTA.h
// ======================= AĞ AYARLARI =======================
const char* WIFI_SSID=internetisminiz";
const char* WIFI_PASS=sifreniz";
IPAddress ipAddr (192, 168, 1, 100);
IPAddress gwAddr (192, 168, 1, 1); // Modem adresi (farklıysa değiştir)
IPAddress maskAddr(255, 255, 255, 0);
IPAddress dns1Addr(192, 168, 1, 1);
IPAddress dns2Addr(8, 8, 8, 8);
const long GMT_OFFSET_SEC=3 * 3600; // Türkiye UTC+3
// ======================= PİNLER =======================
const uint8_t PIN_HEATER=5; // D1
const uint8_t PIN_LIGHT =4; // D2
const uint8_t PIN_TEMP =0; // D3
#define RELAY_ON LOW
#define RELAY_OFF HIGH
// RÖLE SÜRME YÖNTEMİ
// 0=Klasik: AÇIK - LOW, KAPALI - HIGH (pin hep OUTPUT).
// Bu röle modüllerinde 3.3V HIGH rölenin bırakması için yetmiyor (hep açık kalıyor).
// 1=Açık-uçlu: AÇIK - LOW, KAPALI - pin boşa alınır (INPUT). Röleler bırakır.
// Not: Bu modda röle girişi kapalıyken ~4V'ta kalıp hafif ses çıkarabiliyor.
#define RELAY_OPEN_DRAIN 1
void relayWrite(uint8_t pin, bool on) {
#if RELAY_OPEN_DRAIN
if (on) {
digitalWrite(pin, LOW);
pinMode(pin, OUTPUT);
} else {
pinMode(pin, INPUT);
}
#else
digitalWrite(pin, on ? LOW : HIGH);
pinMode(pin, OUTPUT);
#endif
}
// ======================= ISITICI / GÜVENLİK AYARLARI =======================
const float TARGET_MIN =20.0; // Web'den ayarlanabilecek en düşük hedef
const float TARGET_MAX =32.0; // Web'den ayarlanabilecek en yüksek hedef
const float TARGET_DEFAULT =25.0;
const float HYSTERESIS =0.3; // Aç ile kapat arasındaki fark
const float EARLY_OFF =0.2; // Isıtıcıyı hedefin bu kadar altında kapat (cam ısı vermeye devam eder)
// 100W/15L için 0.2. 25-50W ısıtıcıda 0.0 yapabilirsin.
const float ABS_MAX_C =33.5; // Bunun üstünde ne olursa olsun ısıtıcı kapalı + alarm
const unsigned long MAX_ON_MS =10UL * 60UL * 1000UL; // Tam güç eşdeğeri en fazla 10 dk ısıtma (POWER_PCT düşükse süre orantılı uzar)
const unsigned long COOLDOWN_MS =3UL * 60UL * 1000UL; // Sonra 3 dk zorunlu mola
// GÜÇ SINIRI: Web arayüzünden seçilir (100=tam güç, 50=yarım güç) ve kaydedilir.
// Yarım güçte ısıtıcı, 5 dk'lık dilimlerin yarısı kadar açık kalır (2,5 dk açık, 2,5 dk kapalı).
// Tam güçte röle en az tıklar. Aşağıdaki değer sadece ilk kurulumdaki varsayılandır.
const uint8_t POWER_PCT_DEFAULT=50;
const unsigned long PULSE_PERIOD_MS=300000UL;
const unsigned long SENSOR_TIMEOUT_MS=10000; // 10 sn geçerli okuma yoksa kapat
const unsigned long READ_INTERVAL_MS =3000;
// ======================= KALICI AYARLAR =======================
struct Settings {
uint32_t magic;
float targetC;
uint16_t lightOn; // gece yarısından beri dakika
uint16_t lightOff;
uint8_t powerPct; // 50 veya 100
};
const uint32_t MAGIC=0xA11C0DE1;
Settings cfg;
// ======================= DURUM =======================
ESP8266WebServer server(80);
OneWire oneWire(PIN_TEMP);
DallasTemperature sensors(&oneWire);
float tempC=NAN;
bool haveReading=false;
bool sensorOk=false;
unsigned long lastGoodMs=0, lastRequest=0;
bool converting=false;
uint8_t badCount=0;
bool heaterOn=false;
unsigned long heaterOnSince=0;
bool demand=false; // Termostat "ısıt" diyor mu (röle çıkışından bağımsız)
unsigned long demandSince=0;
bool cooling=false;
unsigned long coolStart=0;
const char* hstate=idle";
// --- Sıcaklık kaydı: 5 dakikada bir ortalama nokta, son 24 saat (RAM'de tutulur) ---
const unsigned long LOG_INTERVAL_MS=5UL * 60UL * 1000UL;
const uint16_t LOG_SIZE=288; // 288 x 5 dk=24 saat
const int16_t LOG_INVALID=-32768;
int16_t logTemp[LOG_SIZE]; // 0.1 °C cinsinden
uint8_t logHeat[LOG_SIZE]; // o aralıkta ısıtıcının açık kaldığı yüzde
uint16_t logHead=0, logCount=0;
float accSum=0;
uint16_t accN=0;
unsigned long accHeatMs=0, lastAccMs=0, logStartMs=0;
int lastLogMin=-1; // son kaydın günün kaçıncı dakikası olduğu (-1: saat yok)
bool lightOn=false; // Işığın mantıksal durumu (arayüzde görünen)
bool lightRelay=false; // Rölenin fiziksel durumu
bool blinkInvert=false; // Alarm yanıp sönmesi sırasında röleyi tersler
// --- Alarm uyarısı: alarm varken ışık 3 kez yanıp söner ---
const unsigned long ALARM_REPEAT_MS=120000UL; // Alarm sürerken uyarıyı tekrar etme aralığı (2 dk)
const unsigned long BLINK_STEP_MS =400UL; // Her yanma/sönme süresi
int8_t blinkStep=-1; // -1: yanıp sönme yok
unsigned long blinkNext=0, lastAlarmBlink=0;
bool prevAlarm=false;
// --- Soğuma izleme: su hedeften soğuk kalıp ısınmıyorsa (ısıtıcı bozuk ya da fişi çıkık) alarm ---
const float COLD_MARGIN =1.0; // Hedefin bu kadar altına inince izlemeye başla
const float COLD_MIN_RISE=0.3; // İzleme süresinde en az bu kadar ısınmalı
const unsigned long COLD_WATCH_MS=60UL * 60UL * 1000UL; // 60 dk
bool coldWatch=false, coldAlarm=false;
unsigned long coldSince=0;
float coldRef=0;
// --- WiFi ile kod yükleme (OTA) ---
const char* OTA_HOSTNAME=akvaryum";
const char* OTA_PASSWORD=akvaryum"; // Arduino IDE yüklerken bu şifreyi sorar
bool otaBusy=false, otaStarted=false;
uint8_t lightMode=0; // 0=otomatik, 1=elle açık, 2=elle kapalı (açılışta otomatiğe döner)
// ======================= WEB ARAYÜZÜ =======================
const char INDEX_HTML[] PROGMEM=R"rawliteral(
!DOCTYPE html
html lang=tr"head
meta charset=utf-8"
meta content=width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no,viewport-fit=cover"
meta content=#062b3a"
title🐠 Akvaryum Kontrol Merkezi/title
style
:root{--abyss:#062b3a;--tide:#13707f;--foam:#eaf7f5;--mute:#8dbdc3;--lamp:#f2c879;--coral:#ff8a5b;--ok:#7fe0d2;--bad:#ff6b7a}
*{box-sizing:border-box;-webkit-tap-highlight-color:transparent}
[hidden]{display:none!important}
body{margin:0;min-height:100vh;min-height:100dvh;background:var(--abyss);color:var(--foam);
font:16px/1.45 ui-rounded,"SF Pro Rounded",Nunito,system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;
padding:max(12px,env(safe-area-inset-top)) 14px max(28px,env(safe-area-inset-bottom))}
button,input{font-family:inherit}
button:focus-visible,input:focus-visible{outline:2px solid var(--lamp);outline-offset:2px}
main{max-width:460px;margin:0 auto}
h1{font-size:20px;margin:6px 4px 12px;display:flex;align-items:center;gap:8px}
.dot{width:10px;height:10px;border-radius:50%;background:var(--bad);margin-left:auto}
.dot.ok{background:var(--ok)}
.alarm{margin:0 0 12px;padding:12px 14px;border-radius:16px;background:rgba(255,107,122,.18);border:1px solid var(--bad);font-weight:600}
.tank{position:relative;overflow:hidden;isolation:isolate;border-radius:30px;padding:28px 20px 22px;text-align:center;
background:linear-gradient(180deg,#0e6070 0%,#08394a 100%);border:1px solid rgba(255,255,255,.1)}
.tank::before,.tank::after{content:"";position:absolute;left:0;right:0;z-index:-1;opacity:0;transition:opacity .8s;pointer-events:none}
.tank::before{top:0;bottom:0;background:radial-gradient(120% 70% at 50% -10%,rgba(242,200,121,.5),transparent 65%)}
.tank::after{bottom:0;height:50%;background:linear-gradient(0deg,rgba(255,138,91,.5),transparent)}
.tank.lit::before,.tank.heating::after{opacity:1}
.bub{position:absolute;bottom:-20px;width:var(--s);height:var(--s);border-radius:50%;border:1.5px solid rgba(255,255,255,.4);
opacity:0;animation:rise var(--d) linear infinite;animation-delay:var(--w)}
.tank.heating .bub{opacity:1}
@keyframes rise{to{transform:translateY(-320px)}}
@media (prefers-reduced-motion:reduce){.bub{animation:none}}
.temp{font-size:84px;font-weight:800;letter-spacing:-3px;line-height:1;font-variant-numeric:tabular-nums}
.temp small{font-size:28px;font-weight:600;letter-spacing:0;margin-left:4px;opacity:.7}
.sub{margin:8px 0 16px;color:var(--mute);font-size:15px}
.pill{display:inline-block;padding:7px 14px;border-radius:999px;font-weight:600;font-size:15px;background:rgba(255,255,255,.12)}
.pill.hot{background:var(--coral);color:#3a1204}
.pill.ok{background:rgba(127,224,210,.2);color:var(--ok)}
.pill.warn,.pill.on{background:var(--lamp);color:#3a2a05}
.pill.bad{background:var(--bad);color:#3b0710}
.pill.sm{font-size:13px;padding:4px 10px;margin-left:auto}
.panel{margin-top:14px;padding:16px;border-radius:22px;background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.08)}
h2{font-size:17px;margin:0 0 14px;display:flex;align-items:center;gap:8px}
.step{display:flex;align-items:center;justify-content:space-between;gap:10px}
.rb{width:64px;height:64px;border-radius:20px;border:0;background:rgba(255,255,255,.12);color:var(--foam);font-size:32px;font-weight:600}
.rb:active{background:rgba(255,255,255,.24)}
.tgt{font-size:44px;font-weight:800;font-variant-numeric:tabular-nums}
.tgt small{font-size:20px;font-weight:600;color:var(--mute);margin-left:2px}
.chips{display:flex;gap:8px;margin:14px 0}
.chip{flex:1;min-height:44px;border-radius:14px;border:1px solid rgba(255,255,255,.2);background:transparent;color:var(--foam);font-size:16px;font-weight:600}
.chip.on{background:var(--ok);color:#06302b;border-color:var(--ok)}
.go{width:100%;min-height:52px;border:0;border-radius:16px;font-size:17px;font-weight:700}
.go.mint{background:var(--ok);color:#06302b}
.go.amber{background:var(--lamp);color:#3a2a05}
.go:disabled{opacity:.35}
.seg{display:grid;grid-template-columns:repeat(3,1fr);gap:6px;padding:5px;border-radius:18px;background:rgba(0,0,0,.25)}
.seg button{min-height:46px;border:0;border-radius:13px;background:transparent;color:var(--foam);font-size:14.5px;font-weight:600}
.seg button.on{background:var(--lamp);color:#3a2a05}
.times{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin:14px 0}
.times label{display:flex;flex-direction:column;gap:6px;font-size:14px;color:var(--mute)}
.times input{width:100%;min-height:52px;border-radius:14px;border:1px solid rgba(255,255,255,.2);background:rgba(0,0,0,.25);
color:var(--foam);font-size:20px;font-weight:600;padding:0 12px;color-scheme:dark}
.seg.mint button.on{background:var(--ok);color:#06302b}
.seg.two{grid-template-columns:1fr 1fr}
.seg.hot button.on{background:var(--coral);color:#3a1204}
.chart{margin-top:12px}
.chart svg{width:100%;height:auto;display:block;touch-action:pan-y;user-select:none;-webkit-user-select:none}
.gr{stroke:rgba(255,255,255,.1);stroke-width:1}
.ax{fill:var(--mute);font-size:10px}
.cl{fill:none;stroke:var(--ok);stroke-width:2.5;stroke-linejoin:round;stroke-linecap:round}
.tl{stroke:var(--lamp);stroke-width:1.2;stroke-dasharray:4 4;opacity:.85}
.hb{fill:var(--coral);opacity:.55}
.cur{stroke:var(--foam);stroke-width:1;opacity:.6}
.cc{fill:var(--ok);stroke:var(--abyss);stroke-width:2}
.empty{padding:34px 8px;text-align:center;color:var(--mute)}
.info{min-height:22px;margin-top:6px;font-size:14px;text-align:center;font-variant-numeric:tabular-nums}
.legend{display:flex;gap:14px;justify-content:center;margin-top:6px;font-size:12px;color:var(--mute)}
.legend span::before{content:"";display:inline-block;width:12px;height:4px;border-radius:2px;margin-right:5px;vertical-align:middle}
.l1::before{background:var(--ok)}.l2::before{background:var(--lamp)}.l3::before{background:var(--coral)}
.stats{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-top:12px}
.stats div{padding:10px 12px;border-radius:14px;background:rgba(0,0,0,.2);display:flex;flex-direction:column}
.stats small{font-size:12px;color:var(--mute)}
.stats b{font-size:20px;font-variant-numeric:tabular-nums}
.hint{margin:12px 0 0;font-size:12px;color:var(--mute)}
footer{display:flex;justify-content:space-between;margin:18px 6px 0;font-size:13px;color:var(--mute)}
.toast{position:fixed;left:50%;bottom:max(22px,env(safe-area-inset-bottom));transform:translate(-50%,30px);padding:12px 18px;
border-radius:14px;background:var(--foam);color:#06302b;font-weight:600;opacity:0;pointer-events:none;transition:.25s}
.toast.show{opacity:1;transform:translate(-50%,0)}
/style/head
bodymain
h1🐠 Akvaryum Kontrol Merkezi i/i/h1
div hidden/div
section
i/i
i/i
i/i
i/i
i/i
i/i
i/i
divspan--.-/spansmall°C/small/div
divHedef --/div
divBağlanıyor…/div
/section
section
h2📈 Sıcaklık geçmişi/h2
div
button data-n=72"6 saat/button
button data-n=144"12 saat/button
button data-n=288"24 saat/button
/div
divdivYükleniyor…/div/div
div/div
divspanSıcaklık/spanspanHedef/spanspanIsıtıcı açık/span/div
div hidden
divsmallEn düşük/smallb--/b/div
divsmallOrtalama/smallb--/b/div
divsmallEn yüksek/smallb--/b/div
divsmall🔥 Isıtıcı açık/smallb--/b/div
/div
pHer 5 dakikada bir nokta eklenir. Cihaz yeniden başlayınca geçmiş sıfırlanır./p
/section
section
h2🎯 Hedef sıcaklık/h2
div
button aria-label=Azalt"−/button
divspan--.-/spansmall°C/small/div
button aria-label=Artır"+/button
/div
div/div
button disabledHedefi kaydet/button
/section
section
h2⚡ Isıtıcı gücü/h2
div
button🔥 Tam güç/button
button🌿 Yarım güç/button
/div
pYarım güçte ısıtıcı 2,5 dk açık, 2,5 dk kapalı çalışır. Küçük akvaryumda sıcaklık daha dengeli kalır ama ısınma yavaşlar. Tam güçte röle daha az tıklar./p
/section
section
h2💡 Işık span…/span/h2
div
button🤖 Otomatik/button
button☀️ Açık/button
button🌙 Kapalı/button
/div
div
label🌅 Açılışinput type=time" oninput=dl()"/label
label🌇 Kapanışinput type=time" oninput=dl()"/label
/div
button disabledSaatleri kaydet/button
/section
footerspan🕒 --/spanspan📶 --/spanspan⏱ --/span/footer
div/div
/main
script
const $=id=document.getElementById(id);
const clamp=(v,a,b)=Math.min(b,Math.max(a,v));
let S=null,pt=null,dirtyT=false,dirtyL=false;
async function api(p){const r=await fetch(p,{cache:'no-store'});if(!r.ok)throw new Error(await r.text());return r.json()}
function toast(m){const t=$('toast');t.textContent=m;t.classList.add('show');clearTimeout(toast.h);toast.h=setTimeout(()=t.classList.remove('show'),1800)}
function up(x){const d=Math.floor(x/86400),h=Math.floor(x%86400/3600),m=Math.floor(x%3600/60);return d?d+' g '+h+' sa':h?h+' sa '+m+' dk':m+' dk'}
function showTarget(){
$('tgt').textContent=pt.toFixed(1);
$('saveT').disabled=!dirtyT;
document.querySelectorAll('.chip').forEach(b=b.classList.toggle('on',Math.abs(+b.dataset.v-pt)0.05));
}
function adj(d){if(!S)return;pt=clamp(Math.round((pt+d)*10)/10,S.min,S.max);dirtyT=true;showTarget()}
function pick(v){if(!S)return;pt=clamp(v,S.min,S.max);dirtyT=true;showTarget()}
function dl(){dirtyL=true;$('saveL').disabled=false}
async function saveT(){
try{S=await api('/api/temp?t=+pt);dirtyT=false;render();toast('✅ Hedef kaydedildi')}
catch(e){toast('⚠️ Hedef kaydedilemedi')}
}
async function setMode(m){
try{S=await api('/api/light?mode=+m);render()}
catch(e){toast('⚠️ Işık modu değişmedi')}
}
async function setPower(p){
try{S=await api('/api/power?p=+p);render();toast(p===100?'✅ Tam güç seçildi':'✅ Yarım güç seçildi')}
catch(e){toast('⚠️ Güç ayarı değişmedi')}
}
async function saveL(){
try{S=await api('/api/light?on=+encodeURIComponent($('tOn').value)+'&off=+encodeURIComponent($('tOff').value));
dirtyL=false;render();toast('✅ Işık saatleri kaydedildi')}
catch(e){toast('⚠️ Saatler kaydedilemedi, iki saati de seç')}
}
function render(){
const s=S;
$('temp').textContent=s.temp===null?'--.-':s.temp.toFixed(1);
$('sub').textContent=Hedef '+s.target.toFixed(1)+' °C';
const A={sensor:'⚠️ Sıcaklık probu okunamıyor. Güvenlik için ısıtıcı kapalı tutuluyor. Prob kablosunu ve 4.7k direnci kontrol et.',
over:'🚨 Su çok sıcak! Isıtıcıyı fişten çek ve rölenin yapışıp yapışmadığını kontrol et.',
cold:'🥶 Su hedeften soğuk ve uzun süredir ısınmıyor. Isıtıcı çalışmıyor olabilir, fişini ve bağlantıları kontrol et.'};
$('alarm').hidden=!A[s.alarm];$('alarm').textContent=A[s.alarm]||'';
const H={heating:['🔥 Isıtıyor','hot],idle:['😌 Sıcaklık yerinde','ok],sensor:['⛔ Prob yok, ısıtıcı kapalı','bad],
cooldown:['🧊 Güvenlik molası','warn],over:['🚨 Aşırı sıcak, ısıtıcı kapalı','bad]};
const h=H[s.hstate]||H.idle;
$('hpill').textContent=h[0];$('hpill').className=pill '+h[1];
$('tank').classList.toggle('heating',s.heater);
$('tank').classList.toggle('lit',s.light);
if(!dirtyT){pt=s.target}
showTarget();
let lp;
if(s.mode===0&&!s.timeOk)lp=⏳ Saat bekleniyor';
else lp=(s.light?'💡 Açık':'🌙 Kapalı')+(s.mode===0?' (otomatik)':' (elle)');
$('lpill').textContent=lp;$('lpill').className=pill sm'+(s.light?' on':'');
document.querySelectorAll('#seg button').forEach((b,i)=b.classList.toggle('on',i===s.mode));
if(!dirtyL){$('tOn').value=s.on;$('tOff').value=s.off}
$('saveL').disabled=!dirtyL;
$('fTime').textContent=🕒 '+s.now;
$('fWifi').textContent=📶 '+s.rssi+' dBm';
$('fUp').textContent=⏱ '+up(s.up);
document.querySelectorAll('#pw button').forEach((b,i)=b.classList.toggle('on',(i===0?100:50)===s.power));
if(LG&&render.tg!==s.target){render.tg=s.target;drawLog()}
}
async function refresh(){
try{S=await api('/api/status');$('dot').classList.add('ok');render()}
catch(e){$('dot').classList.remove('ok')}
}
let LG=null,rng=288,CH=null;
const f1=v=v.toFixed(1),pad2=n=String(n).padStart(2,'0');
function hm(m){m=((m%1440)+1440)%1440;return pad2(Math.floor(m/60))+':'+pad2(m%60)}
async function loadLog(){try{LG=await api('/api/log');drawLog()}catch(e){}}
function setRng(n){rng=n;document.querySelectorAll('#rng button').forEach(b=b.classList.toggle('on',+b.dataset.n===n));drawLog()}
function drawLog(){
if(!LG)return;
const n=Math.min(LG.t.length,rng),box=$('chart'),st=$('stats');
const T=LG.t.slice(LG.t.length-n),Hh=LG.h.slice(LG.h.length-n);
const vals=T.filter(v=v!==null).map(v=v/10);
if(!vals.length){box.innerHTML=divHenüz kayıt yok. İlk nokta 5 dakika içinde eklenir./div';st.hidden=true;CH=null;$('lgInfo').textContent=';return}
st.hidden=false;
const W=340,HT=180,L=36,R=8,TP=10,B=24,pw=W-L-R,ph=HT-TP-B,tg=S?S.target:null;
let lo=Math.min(...vals,tg===null?99:tg),hi=Math.max(...vals,tg===null?-99:tg);
lo=Math.floor((lo-.3)*2)/2;hi=Math.ceil((hi+.3)*2)/2;
if(hi-lo2){const c=(hi+lo)/2;lo=c-1;hi=c+1}
const X=i=L+(n1?i*pw/(n-1):pw/2),Y=v=TP+ph*(1-(v-lo)/(hi-lo));
let g=';
for(let k=0;k=4;k++){const v=lo+k*(hi-lo)/4,y=Y(v);
g+=`line x1=${L}" x2=${W-R}" y1=${f1(y)}" y2=${f1(y)}"/text x=${L-5}" y=${f1(y+3.5)}" text-anchor=end"${f1(v)}/text`}
const bw=Math.max(1,pw/n*.8);
Hh.forEach((p,i)={if(p0){const h=p/100*24;g+=`rect x=${f1(X(i)-bw/2)}" y=${f1(TP+ph-h)}" width=${f1(bw)}" height=${f1(h)}"/`}});
if(tg!==null)g+=`line x1=${L}" x2=${W-R}" y1=${f1(Y(tg))}" y2=${f1(Y(tg))}"/`;
let d=',pen=false;
T.forEach((v,i)={if(v===null){pen=false;return}d+=(pen?'L':'M')+f1(X(i))+' '+f1(Y(v/10))+(pen?'':' h.01');pen=true});
for(let k=0;k4;k++){
const i=Math.round(k*(n-1)/3),ago=(n-1-i)*LG.step;
const lab=LG.end=0?hm(LG.end-ago):(ago?'-'+Math.round(ago/60)+' sa':'son');
g+=`text x=${f1(X(i))}" y=${HT-6}" text-anchor=${k===0?'start':k===3?'end':'middle'}"${lab}/text`}
box.innerHTML=`svg viewBox=0 0 ${W} ${HT}" role=img" aria-label=Sıcaklık grafiği"${g}path d=${d}"/`+
`line y1=${TP}" y2=${TP+ph}" x1=0" x2=0"/circle r=4"//svg`;
CH={n,T,Hh,X,Y,W,L,pw};
const sv=$('svg');sv.addEventListener('pointerdown',probe);sv.addEventListener('pointermove',probe);
const mn=Math.min(...vals),mx=Math.max(...vals),av=vals.reduce((a,b)=a+b,0)/vals.length;
const mins=Hh.reduce((a,p)=a+p,0)/100*LG.step;
$('sMin').textContent=f1(mn)+'°';$('sAvg').textContent=f1(av)+'°';$('sMax').textContent=f1(mx)+'°';
$('sHeat').textContent=mins=60?Math.floor(mins/60)+' sa '+Math.round(mins%60)+' dk':Math.round(mins)+' dk';
$('lgInfo').textContent=Grafiğe dokun, o andaki değeri gör';
}
function probe(e){
if(!CH)return;
const r=$('svg').getBoundingClientRect(),x=(e.clientX-r.left)/r.width*CH.W;
let i=CH.n1?Math.round((x-CH.L)/CH.pw*(CH.n-1)):0;i=Math.max(0,Math.min(CH.n-1,i));
const v=CH.T[i],cur=$('cur'),cc=$('cc'),px=CH.X(i),ago=(CH.n-1-i)*LG.step;
const when=LG.end=0?hm(LG.end-ago):(ago?(ago=60?Math.round(ago/60)+' sa önce':ago+' dk önce'):'en son');
cur.setAttribute('x1',px);cur.setAttribute('x2',px);cur.style.display=';
if(v===null){cc.style.display=none';$('lgInfo').textContent=🕒 '+when+' veri yok';return}
cc.setAttribute('cx',px);cc.setAttribute('cy',CH.Y(v/10));cc.style.display=';
$('lgInfo').textContent=🕒 '+when+' 🌡️ '+f1(v/10)+' °C 🔥 %'+CH.Hh[i];
}
[24,26,28,30].forEach(v={
const b=document.createElement('button');b.className=chip';b.dataset.v=v;b.textContent=v+'°';
b.onclick=()=pick(v);$('chips').appendChild(b);
});
refresh();loadLog();
setInterval(()={if(!document.hidden)refresh()},3000);
setInterval(()={if(!document.hidden)loadLog()},60000);
document.addEventListener('visibilitychange',()={if(!document.hidden)refresh()});
/script/body/html
)rawliteral";
// ======================= YARDIMCI FONKSİYONLAR =======================
void saveSettings() {
EEPROM.put(0, cfg);
EEPROM.commit();
}
void loadSettings() {
EEPROM.get(0, cfg);
if (cfg.magic != MAGIC || isnan(cfg.targetC) || cfg.targetC TARGET_MIN || cfg.targetC TARGET_MAX ||
cfg.lightOn=1440 || cfg.lightOff=1440) {
cfg.magic =MAGIC;
cfg.targetC =TARGET_DEFAULT;
cfg.lightOn =10 * 60; // 10:00
cfg.lightOff=20 * 60; // 20:00
cfg.powerPct=POWER_PCT_DEFAULT;
saveSettings();
}
// Eski kayıtta güç alanı yoktu: geçersizse varsayılana al (hedef ve ışık saatleri korunur)
if (cfg.powerPct != 50 && cfg.powerPct != 100) {
cfg.powerPct=POWER_PCT_DEFAULT;
saveSettings();
}
}
bool timeOk() { return time(nullptr) 1700000000; }
int nowMinutes() {
time_t t=time(nullptr);
struct tm* lt=localtime(&t);
return lt-tm_hour * 60 + lt-tm_min;
}
String nowString() {
if (!timeOk()) return "--:--:--";
time_t t=time(nullptr);
struct tm* lt=localtime(&t);
char b[9];
snprintf(b, sizeof(b), "%02d:%02d:%02d", lt-tm_hour, lt-tm_min, lt-tm_sec);
return String(b);
}
String fmtHM(int m) {
char b[6];
snprintf(b, sizeof(b), "%02d:%02d", m / 60, m % 60);
return String(b);
}
int parseHM(const String& s) {
int c=s.indexOf(':');
if (c 1) return -1;
int h=s.substring(0, c).toInt();
int m=s.substring(c + 1).toInt();
if (h 0 || h 23 || m 0 || m 59) return -1;
return h * 60 + m;
}
// ======================= SICAKLIK OKUMA (bloklamadan) =======================
void updateTemperature() {
unsigned long now=millis();
if (!converting) {
if (now - lastRequest=READ_INTERVAL_MS) {
sensors.requestTemperatures();
converting=true;
lastRequest=now;
}
} else if (now - lastRequest=800) {
converting=false;
float t=sensors.getTempCByIndex(0);
if (t == DEVICE_DISCONNECTED_C || t == 85.0 || t -20.0 || t 60.0) {
if (++badCount=10) { // ~30 sn hatalı okuma: probu yeniden tara
sensors.begin();
sensors.setWaitForConversion(false);
badCount=0;
}
} else {
tempC=t;
accSum += t;
accN++;
haveReading=true;
lastGoodMs=now;
badCount=0;
}
}
}
// ======================= ISITICI KONTROLÜ =======================
void setHeater(bool on) {
if (on == heaterOn) return;
heaterOn=on;
relayWrite(PIN_HEATER, on);
if (on) heaterOnSince=millis();
Serial.printf("Isitici %s (%.1f C, hedef %.1f)\n", on ? "ACIK" : "KAPALI", tempC, cfg.targetC);
}
void controlHeater() {
unsigned long now=millis();
sensorOk=haveReading && (now - lastGoodMs SENSOR_TIMEOUT_MS);
if (otaBusy) { // Kod yüklenirken ısıtıcı kapalı kalır
setHeater(false);
hstate=idle";
return;
}
if (cooling && now - coolStart=COOLDOWN_MS) {
cooling=false;
demandSince=now; // Mola bitti, süre sayacı baştan başlar
}
hstate=idle";
bool want=false;
if (!sensorOk) {
demand=false; // Prob yoksa ısıtıcı kapalı
hstate=sensor";
} else if (tempC=ABS_MAX_C) {
demand=false; // Mutlak üst sınır
hstate=over";
} else {
// 1) Termostat: ısıtma talebi var mı (histerezisli)
float offAt=cfg.targetC - EARLY_OFF;
bool d=demand;
if (tempC=offAt) d=false;
else if (tempC=offAt - HYSTERESIS) d=true;
if (d && !demand) demandSince=now;
demand=d;
// 2) Süre limiti: talep, tam güç eşdeğeri MAX_ON_MS'yi aşarsa zorunlu mola
unsigned long pct=constrain(cfg.powerPct, 10, 100);
if (demand && !cooling && now - demandSince=MAX_ON_MS * 100UL / pct) {
cooling=true;
coolStart=now;
}
// 3) Çıkış: talep varsa ve mola yoksa, güç sınırına göre darbeli aç
if (demand) {
if (cooling) {
hstate=cooldown";
} else {
hstate=heating";
bool pulseOn=true;
if (pct 100) pulseOn=((now - demandSince) % PULSE_PERIOD_MS) (PULSE_PERIOD_MS * pct / 100);
want=pulseOn;
}
}
}
setHeater(want);
}
// ======================= IŞIK KONTROLÜ =======================
bool scheduleSaysOn() {
if (!timeOk()) return false;
int m=nowMinutes();
int a=cfg.lightOn, b=cfg.lightOff;
if (a == b) return false;
if (a b) return m=a && m b;
return m=a || m b; // Gece yarısını aşan program (örn. 18:00 - 02:00)
}
void controlLight() {
bool want;
if (lightMode == 1) want=true;
else if (lightMode == 2) want=false;
else want=scheduleSaysOn();
if (want != lightOn) {
lightOn=want;
Serial.printf("Isik %s\n", want ? "ACIK" : "KAPALI");
}
bool phys=(want != blinkInvert); // Alarm yanıp sönerken röle tersine çevrilir
if (phys != lightRelay) {
lightRelay=phys;
relayWrite(PIN_LIGHT, phys);
}
}
// ======================= SICAKLIK KAYDI =======================
void logTick() {
unsigned long now=millis();
if (heaterOn) accHeatMs += now - lastAccMs;
lastAccMs=now;
if (now - logStartMs=LOG_INTERVAL_MS) {
logStartMs += LOG_INTERVAL_MS;
int16_t v=accN ? (int16_t)roundf(accSum / accN * 10.0f) : LOG_INVALID;
unsigned long pct=accHeatMs * 100UL / LOG_INTERVAL_MS;
if (pct 100) pct=100;
logTemp[logHead]=v;
logHeat[logHead]=(uint8_t)pct;
logHead=(logHead + 1) % LOG_SIZE;
if (logCount LOG_SIZE) logCount++;
lastLogMin=timeOk() ? nowMinutes() : -1;
accSum=0;
accN=0;
accHeatMs=0;
}
}
void sendLog() {
String j;
j.reserve(3200);
uint16_t start=(logHead + LOG_SIZE - logCount) % LOG_SIZE;
j += F("{\"step\":");
j += (int)(LOG_INTERVAL_MS / 60000UL);
j += F(",\"end\":");
j += lastLogMin;
j += F(",\"t\":[");
for (uint16_t k=0; k logCount; k++) {
int16_t v=logTemp[(start + k) % LOG_SIZE];
if (k) j += ',';
if (v == LOG_INVALID) j += F("null");
else j += (int)v;
}
j += F(],\"h\":[");
for (uint16_t k=0; k logCount; k++) {
if (k) j += ',';
j += (int)logHeat[(start + k) % LOG_SIZE];
}
j += F(]}");
server.sendHeader("Cache-Control", "no-store");
server.send(200, "application/json", j);
}
// ======================= WEB SUNUCU =======================
const char* alarmCode() {
if (sensorOk && tempC=ABS_MAX_C) return "over";
if (!sensorOk && millis() 20000) return "sensor";
if (coldAlarm) return "cold";
return "";
}
// Alarm varken ışığı 3 kez ters çevirip eski haline getirir (bloklamadan).
// Alarm başlar başlamaz hemen bir kez, sonra ALARM_REPEAT_MS aralığıyla tekrar eder.
void alarmBlinkTick() {
unsigned long now=millis();
bool alarm=alarmCode()[0] != '\0';
if (alarm && !prevAlarm) lastAlarmBlink=now - ALARM_REPEAT_MS; // yeni alarm: hemen uyar
prevAlarm=alarm;
if (blinkStep 0) {
if (!alarm || now - lastAlarmBlink ALARM_REPEAT_MS) return;
blinkStep=0;
blinkNext=now;
lastAlarmBlink=now;
}
if ((long)(now - blinkNext) 0) return;
if (blinkStep=6) { // 3 yanıp sönme bitti
blinkInvert=false;
blinkStep=-1;
return;
}
blinkInvert=(blinkStep % 2 == 0); // çift adım: ters çevir, tek adım: eski haline getir
blinkStep++;
blinkNext=now + BLINK_STEP_MS;
}
// Su hedeften soğukta kalıp 60 dk boyunca ısınmıyorsa "cold" alarmı verir.
void coldWatchTick() {
if (!sensorOk) return;
unsigned long now=millis();
if (tempC=cfg.targetC - COLD_MARGIN) {
if (!coldWatch) {
coldWatch=true;
coldSince=now;
coldRef=tempC;
} else if (tempC=coldRef + COLD_MIN_RISE) { // Isınıyor: pencereyi yenile
coldRef=tempC;
coldSince=now;
coldAlarm=false;
} else if (now - coldSince=COLD_WATCH_MS) {
coldAlarm=true;
}
} else {
coldWatch=false;
coldAlarm=false;
}
}
// ======================= WIFI İLE KOD YÜKLEME (OTA) =======================
void startOta() {
ArduinoOTA.setHostname(OTA_HOSTNAME);
ArduinoOTA.setPassword(OTA_PASSWORD);
ArduinoOTA.onStart([]() {
otaBusy=true;
setHeater(false); // Yükleme sırasında ısıtıcı kapalı
Serial.println("OTA basladi: isitici kapatildi");
});
ArduinoOTA.onEnd([]() { Serial.println("OTA bitti"); });
ArduinoOTA.onError([](ota_error_t e) {
otaBusy=false;
Serial.printf("OTA hata: %u\n", (unsigned)e);
});
ArduinoOTA.begin();
otaStarted=true;
Serial.println("OTA hazir");
}
String statusJson() {
String j;
j.reserve(420);
j += F("{\"temp\":"); j += sensorOk ? String(tempC, 1) : String("null");
j += F(",\"target\":"); j += String(cfg.targetC, 1);
j += F(",\"min\":"); j += String(TARGET_MIN, 1);
j += F(",\"max\":"); j += String(TARGET_MAX, 1);
j += F(",\"power\":"); j += (int)cfg.powerPct;
j += F(",\"heater\":"); j += heaterOn ? "true" : "false";
j += F(",\"hstate\":\""); j += hstate; j += '"';
j += F(",\"light\":"); j += lightOn ? "true" : "false";
j += F(",\"mode\":"); j += (int)lightMode;
j += F(",\"on\":\""); j += fmtHM(cfg.lightOn); j += '"';
j += F(",\"off\":\""); j += fmtHM(cfg.lightOff); j += '"';
j += F(",\"timeOk\":"); j += timeOk() ? "true" : "false";
j += F(",\"now\":\""); j += nowString(); j += '"';
j += F(",\"alarm\":\""); j += alarmCode(); j += '"';
j += F(",\"rssi\":"); j += (long)WiFi.RSSI();
j += F(",\"up\":"); j += (unsigned long)(millis() / 1000);
j += '}';
return j;
}
void sendStatus() {
server.sendHeader("Cache-Control", "no-store");
server.send(200, "application/json", statusJson());
}
void handleRoot() {
server.send_P(200, PSTR("text/html; charset=utf-8"), INDEX_HTML);
}
void handleTemp() {
if (!server.hasArg("t")) { server.send(400, "text/plain", "t eksik"); return; }
float t=server.arg("t").toFloat();
if (t TARGET_MIN || t TARGET_MAX) { server.send(400, "text/plain", "aralik disi"); return; }
cfg.targetC=roundf(t * 10.0f) / 10.0f;
saveSettings();
Serial.printf("Yeni hedef: %.1f C\n", cfg.targetC);
sendStatus();
}
void handlePower() {
if (!server.hasArg("p")) { server.send(400, "text/plain", "p eksik"); return; }
int p=server.arg("p").toInt();
if (p != 50 && p != 100) { server.send(400, "text/plain", "50 veya 100"); return; }
cfg.powerPct=p;
saveSettings();
Serial.printf("Isitici gucu: %%%d\n", p);
sendStatus();
}
void handleLight() {
if (server.hasArg("mode")) {
int m=server.arg("mode").toInt();
if (m 0 || m 2) { server.send(400, "text/plain", "mod hatali"); return; }
lightMode=m;
}
if (server.hasArg("on") || server.hasArg("off")) {
int a=parseHM(server.arg("on"));
int b=parseHM(server.arg("off"));
if (a 0 || b 0) { server.send(400, "text/plain", "saat hatali"); return; }
cfg.lightOn=a;
cfg.lightOff=b;
saveSettings();
}
controlLight();
sendStatus();
}
// ======================= WIFI =======================
void connectWifi() {
WiFi.mode(WIFI_STA);
WiFi.config(ipAddr, gwAddr, maskAddr, dns1Addr, dns2Addr);
WiFi.begin(WIFI_SSID, WIFI_PASS);
}
void checkWifi() {
static unsigned long lastTry=0;
if (WiFi.status() == WL_CONNECTED) return;
if (millis() - lastTry 20000) {
lastTry=millis();
WiFi.disconnect();
connectWifi();
}
}
// ======================= SETUP / LOOP =======================
void setup() {
// Röleleri en başta KAPALI yap
relayWrite(PIN_HEATER, false);
relayWrite(PIN_LIGHT, false);
Serial.begin(115200);
Serial.println();
EEPROM.begin(64);
loadSettings();
sensors.begin();
sensors.setWaitForConversion(false);
lastRequest=millis() - READ_INTERVAL_MS;
logStartMs=lastAccMs=millis();
WiFi.persistent(false);
WiFi.setAutoReconnect(true);
WiFi.setSleepMode(WIFI_NONE_SLEEP);
connectWifi();
unsigned long t0=millis();
while (WiFi.status() != WL_CONNECTED && millis() - t0 20000) {
delay(250);
}
if (WiFi.status() == WL_CONNECTED) {
Serial.print("Baglandi: ");
Serial.println(WiFi.localIP());
} else {
Serial.println("WiFi baglanamadi, arka planda denenecek");
}
configTime(GMT_OFFSET_SEC, 0, "pool.ntp.org", "time.google.com", "time.cloudflare.com");
server.on("/", handleRoot);
server.on("/api/status", sendStatus);
server.on("/api/temp", handleTemp);
server.on("/api/light", handleLight);
server.on("/api/power", handlePower);
server.on("/api/log", sendLog);
server.on("/favicon.ico", []() { server.send(204); });
server.onNotFound([]() { server.send(404, "text/plain", "Bulunamadi"); });
server.begin();
}
void loop() {
ArduinoOTA.handle();
if (!otaStarted && WiFi.status() == WL_CONNECTED) startOta();
server.handleClient();
updateTemperature();
controlHeater(); // WiFi olmasa bile çalışır
alarmBlinkTick();
controlLight();
logTick();
coldWatchTick();
checkWifi();
}
[/QUOTE][EDIT]NightWist,2026-10-24 18:40:22[/EDIT]
Yem Atıkları İçin Salyangoz Önerisi
[QUOTE=SaRCoPhiLiuS]Yani bunu gözlemlemek lazım. Şu kadar bu kadar diye kesin birşeyler söylemek yanlış olur. 10 tane ekleyin, gözlemleyin. 2-3 aydır yemsiz yaşıyor sizin sanrınız. Dibe çöken yemleri yiyorlardır.[;)][/QUOTE]
Yem atmıyorum. Akvaryumda başka canlı yok.
[QUOTE=SaRCoPhiLiuS]Yani bunu gözlemlemek lazım. Şu kadar bu kadar diye kesin birşeyler söylemek yanlış olur. 10 tane ekleyin, gözlemleyin. 2-3 aydır yemsiz yaşıyor sizin sanrınız. Dibe çöken yemleri yiyorlardır.[;)][/QUOTE]
Yem atmıyorum. Akvaryumda başka canlı yok.
Yem Atıkları İçin Salyangoz Önerisi
[QUOTE=SaRCoPhiLiuS]Ramshorn. Tek sorunu istilacıdırlar. Çok çoğalınca 1-2 helena salyangozu alırsınız, denge sağlanır. Elma salyangozu, evet tüketimi iyidir ama elma salyangozuna çok benzeyen kardeşi bitkileri yer ve elma salyangozları da yedikçe pislik üretir. Büyük oldukları için tavsiye etmem.
Poso'lar kuma gömülme, girme güdüsüne sahip en büyük salyangoz türlerinden. Bitkileri sökerler ve mosslara dadanabilirler.
Ramshorn sizi memnun edecektir.[/QUOTE]
Akvaryumda iki tane helena var kaç tane atsam soyunu kurutmazlar sizce? 15 litre akvaryum. 2-3 aydır yemsiz yaşıyor helenalar ne yiyorlar hiç bir fikrim yok. 1 Nerite 2 Helena iki aydır kardeş kardeş takılıyorlar.[EDIT]NightWist,2026-10-13 18:27:42[/EDIT]
[QUOTE=SaRCoPhiLiuS]Ramshorn. Tek sorunu istilacıdırlar. Çok çoğalınca 1-2 helena salyangozu alırsınız, denge sağlanır. Elma salyangozu, evet tüketimi iyidir ama elma salyangozuna çok benzeyen kardeşi bitkileri yer ve elma salyangozları da yedikçe pislik üretir. Büyük oldukları için tavsiye etmem.
Poso'lar kuma gömülme, girme güdüsüne sahip en büyük salyangoz türlerinden. Bitkileri sökerler ve mosslara dadanabilirler.
Ramshorn sizi memnun edecektir.[/QUOTE]
Akvaryumda iki tane helena var kaç tane atsam soyunu kurutmazlar sizce? 15 litre akvaryum. 2-3 aydır yemsiz yaşıyor helenalar ne yiyorlar hiç bir fikrim yok. 1 Nerite 2 Helena iki aydır kardeş kardeş takılıyorlar.[EDIT]NightWist,2026-10-13 18:27:42[/EDIT]
SON MESAJLAR
- Bir Catappa'nın Günlüğü uozgul, 21:57
Bitki Türleri ve Bakımı - Betta Selahaddin Mirza Adar, 21:37
Akvaryum Tanıtımı - Bitkili Akvaryum Dry Start Başlangıç Kurulumu Garajmoda, 21:24
Bitki Akvaryumları Genel - Cüce Vatoz crak, 21:21
Sazansıgiller - Beta Balığı İçin Ufak Akvaryum Tasarlama Mr.Magic, 21:11
Akvaryum Tasarımı ve Dekorasyonu - Sobo Wp 628 Ses Gürültüsü Hammet, 21:02
Yeni Üye Forumu - Akvaryumdaki Suyum Köpükleniyor Betta_King, 20:18
Akvaryum ve Tür Tavsiyesi - Bu Mülteciyi Ne Yapacağımı Bilemedim GhostKoi, 20:00
Sazansıgiller - İlk Kez Tasarımını Yaptığım Tankım. Erdinc.B, 19:52
Akvaryum Tasarımı ve Dekorasyonu - Betta Nasıl Bakılmalı Mr.Magic, 18:58
Akvaryum ve Tür Tavsiyesi - Albino Koi Lepistesler Mirza Adar, 18:26
Akvaryum Tanıtımı - Geophagus Altifronsum Hasta Ghuney, 16:35
Hastalıklar ve İlaçlar - Dennerle Deponit Mix Pro Mu?tropica Substrate Mi? Ahmet53, 15:47
Akvaryum Tasarımı ve Dekorasyonu - Strong 70 Den Beklenmeyen Hareket Dikkat! Gld5, 15:06
Akvaryumcu Önerileri ve Duyuruları - Hareketsiz Japonlar Betta_King, 14:25
Yeni Üye Forumu - Canlipetshop Ağır Şikayet hvostr, 12:56
Şikayet Var! - Japon Balıklarımı Ne Yapmalıyım? aqualife1995, 11:17
Yeni Üye Forumu - Betam İyi Değil :( rakun4, 10:14
Yeni Üye Forumu - Silivri'li akvaristler feneriumum, 09:37
İstanbullu Akvaristler - Vernikli Salyangoz Kabuğunu Akvaryuma Koymak Balıkmen, 09:10
Malawi ve Diğer Afrika Cichlidleri - Cryptocoryne Parva Bitkisi kayra37, 01:44
Yeni Üye Forumu - Canlı Önerisi JuqLax, 00:08
Akvaryum ve Tür Tavsiyesi - Tetra Marka Dış Filtre Alınır Mı? capello, 23:02
Akvaryum ve Tür Tavsiyesi - 🇹🇷 29 Ekim Cumhuriyet Bayramımız Kutlu Olsun! hoppala, 22:05
Forum ve Site Hakkında - 80 30 40h Low Tech Bitki Gübre Ve Işık Önerisi Shadowzen, 21:59
Yeni Üye Forumu - Lepistes Hamile Mi? Aqua Discovery, 20:56
Canlı Doğuranlar - Yavrularım Ölüyor Önüne Nasıl Geçerim Erd4an, 20:22
Yeni Üye Forumu - Lepistes Her Gün Kayıp Yaşıyorum. Neden? ~Emre, 18:35
Hastalıklar ve İlaçlar - Karides Nasıl Üretilir Ve Bakabilirum M.kari, 18:19
Yeni Üye Forumu - Discus Tavsiye - 120x45x45 Makkaya, 17:42
Akvaryum ve Tür Tavsiyesi - Electric Blue & German Blue Ramirazi darkaura, 17:23
Cinsiyet ve Tür Belirleme - Transfer İçin Isıtıcı DenizAslan, 15:26
Üye Portfolyo - Yeni Evinde Mutlu Mu? DenizAslan, 15:19
Yeni Üye Forumu - Petshopların Genel Kalitesinin Düşüşü hvostr, 14:35
Şikayet Var! - Hightech Tasarım Akvaryumu braweheart, 13:49
Akvaryum Tanıtımı - Beyaz Yunusmu Mavi Yunusmu Balıkmen, 13:42
Malawi ve Diğer Afrika Cichlidleri - Pipo Filtrenin Faydaları? Betta_King, 12:39
Filtreleme Seçenekleri - Akvaryum İsmail saynak, 07:27
Akvaryum Tanıtımı - Gül Barbus Alıştırma Sonrası Dipte Yüzüyor SalvadorDalli, 06:42
Hastalıklar ve İlaçlar - Brikardi Beslemek İstiyorum Öneri ykferah, 23:06
Yeni Üye Forumu - Kafa Motoru Uğultu Ve Ses Problemi Mert1980, 22:02
Yeni Üye Forumu - Betta İçin İdeal Yaşam Alanı? hvostr, 21:16
Yeni Üye Forumu - İlanım Başka Sitede De Yayınlanmış hvostr, 21:14
Şikayet Var! - Anadolu Akvaryum Pet Ürünleri capodicapii, 20:21
Şikayet Var! - Beta Bakımı Betta_King, 16:17
Akvaryum ve Tür Tavsiyesi - Lepistes Fiyatlarındaki Gereksz Artış! İnci Kefali, 15:50
Şikayet Var! - Emas Akvaryum Şikayet lodostm, 14:56
Şikayet Var! - Canlı Petshop Mağduru Bitmez lodostm, 14:55
Şikayet Var! - Çayırova Akvaryum Tahakuru, 14:49
Şikayet Var! - Eheim Compact 2100 Kafa Motoru Ses Problemi Mert1980, 14:46
Filtreleme Seçenekleri
GÜNCEL 100 TANITIM
SON İLANLAR
- Taşınma Nedeniyle Toplu Satış ***çok Uygun*** Kfat 22:45
- Lepistes Endler, Yerli Üretim 20 Tl Birtek 22:38
- Filitre Motor Mili Ve Ehaim Parçaları Arıyorum Birtek 22:38
- Crypto Wendti Green Brown. Birtek 22:38
- Afrika Cichlidleri devito 22:22
- Kullanılmış İsta Premium Soil Brsk 22:14
- Orjinal Sajica Cichlid Çiftiler Ve Gençler -RBG- 22:09
- Crptocoryne Crispatula Var SeaWorld 22:08
- Cüce Vatoz & L144 SeaWorld 22:08
- Auratus, Cüce Vatoz, L144, Polar Blue Parrot oguzozt 21:52
- 8 Çıkışlı 45w Hava Motoru oguzozt 21:52
- 🌿 Bitkili Akvaryumlar İçin Akıllı Kapsül Gübre purelyfe 21:51
- Tetra Pro Algae Yeni Kova teskere 21:06
- Ramshorn Salyangoz teskere 21:06
- Karışık Bitkiler emregungor 20:27
- Christmass Moss emregungor 20:27
- Power Led Bitki Armatürü (70 Cm 24 Watt) emregungor 20:27
- Candidi Fazla Erkekler Deep83 19:14
- Ramirezi Çift Arıyorum Lodos muhafızı 17:53
- 2 Adet Kuhli Loach biars 17:13
- Tanganyikanın Nadir Güzellikleri muhammet abaci 15:51
- Anubias- Christmassmoss- Cryptocoryne Wendtii- Saz kopruluonur 15:49
- Tül Kuyruk Vatoz Türleri / Hb White Lepistes kopruluonur 15:49
- İhtiyaç Fazlası Akvaryum Malzemeleri kopruluonur 15:49
- Bucephalandra ALP85 15:09
- Katil Salyangozlarla. ÖĞRÜNÇ 15:08
- Mini Pellia Moss FurkanYnk 14:43
- Mangrow Üstü Anubias(yenilendi), Cüce Cyrptocoryne nikon_ 14:16
- 2 Adet Japon Balığı (orta Boy) metin384 14:12
- Ücretsiz 2 Adet Japon Balığı metin384 14:12
- Su Piresi 200 - 300 Adet 100 Tl CevdetSERBEST 14:05
- Exel , Ramshorm , Bitki CevdetSERBEST 14:05
- Moss Teli CevdetSERBEST 14:05
- Melek Balığı CevdetSERBEST 14:05
- Diy Gübreler Kargo Bedava Bitkiler, Balıklar reano 13:48
- Karides ,vatoz, Bitki Çeşitleri, Gübre reano 13:48
- 4 Lü Akvaryum Seti ( Alüminyum Profil) soliter83 13:18
- Moss Kafesi, Co2 Difizör Ve Kiti, Betta Ayna Ve Ha soliter83 13:18
- Satılık Rainbow Ciklet Ve Ellioti (keçiören) Blocktrade 12:39
- Su Piresi & Yeşil Su & Infusoria Amati340 12:35
- Ramshorn Salyangoz (10 Adet) Amati340 12:35
- Osmocote Akıllı Kapsül Gübre ( 9 Ay Etkili) Amati340 12:35
- Microfex( Dero Worm) & Sirke Kurdu Amati340 12:35
- Springtail Kültürü (sıçrarkuyruk) Amati340 12:35
- L144 Mavi Göz Tül Kuyruk Cüce Vatoz Yavrusu emree06 12:28
- Flame Moss , Java Moss, Pothos Sarmaşığı gyunda 12:25
- Damızlık Koi Melek,tiger Endler,karides gyunda 12:25
- Akvaryum , Su Piresi , Balık Yemi gyunda 12:25
- Frontosa Burindi 4-4.5cm, Pleco, Tavşan Salyangoz. voltran 12:21
- Sarı Prensesler +mavi Prensesler Balestra 11:29
KULLANICILAR
- 105 Online Kullanıcı, 57 Üye
Online Kullanıcılar
oguzozt,
Saltuk Buğrahan,
Tatar10,
kaantfk,
Ra93,
Słowik28,
ErenYildiz,
reano,
Naturality00,
berkant68,
Akvarist Hoca,
mert0310,
serikli,
Sütçü,
Atooken,
Onul2,
MiSoSiKiaN,
-RBG-,
selcukpoyrazkaya,
muratabi54,
Garajmoda,
Shadowzen,
Ugipi,
ismail0532,
Yusuf77777,
AqualandDE,
pianoƒish,
meraklimelahat,
Birtek,
metin384,
NSRTNT,
Resifci,
iakyol,
stimbil,
yusuf efe,
MCY4,
sero456,
Ghuney,
dreamxx,
Zacharypaf,
Simyaci00,
CenanMurat,
Taha⚡,
Fang,
Kürşat Koç,
dreamcatcherr,
devito,
CevdetSERBEST,
MehmetGok,
Murattyldz1,
lonewisp,
Ahmet53,
Kifidis,
Pumaisher70,
Berkay Koç,
Goldfish2000,
MertSoyumert,
hipehlivan1,
FORUM İSTATİSTİKLERİ
- 3,797,668 Mesaj
- 408,613 Konu
- 91 Forum
- 145,128 Forum Üyesi
- 1,466 Özel Forum Üyesi
- 29 Kıdemli Akvarist
- 1,941 Dün Giriş Yapan Üye
Şu ana kadar en fazla 1365 kişi 27.03.2012 23:21 tarih ve saatinde çevrim içi oldu.
