Hapiçe – Otomatik İlaç Makinesi

Merhabalar. İlaçları düzenli kullanmak önemli elbette. Bazı hastalıklar için ilaçları tam saatinde almak çok daha önemli olabiliyor. Kimse ilacını almayı unutanlar için bir cihaz tasarladık. Aslına bakarsanız bir şey deniyorum. Bildiğiniz gibi tüketici elektroniği ürünleri çok genel bir işlev sunuyor. Kendi ihtiyaçlarımız için kendi cihazlarımızı yaptığımızda ise hiçbir markanın sunamayacağı esneklikte bir makine elde ediyoruz. Yaptığımız cihaza istediğimiz özelliği ekleyip istemediğimizi çıkarabiliyoruz ve bunu yapmak sandığınızdan daha kolay ve eğlenceli. Benim amacımsa bu bilmediğiniz dünyanın kapılarını arayıp lezzetini tatmanızı sağlamak. Kendi ellerinizle bir şey inşaa ettiğinizde ve bir kahve alıp tıkır tıkır çalışmasını izlediğinizde oluşacak tatmin hissini çok daha fazla kişiye deneyimletmeyi hedefliyorum.

Hapiçe büyük bir projenin ilk denemesidir. Bunun gibi pek çok eğlenceli cihaz tasarlayıp tüm kaynakları herkesin yapabilmesi için ücretsiz paylaşmayı hedefliyorum. Ayrıca hiç bilmeyenler veya 3d baskı imkanı olmayanlar için proje için gereken baskılar, kablolar, kartlar kısacası aklınıza gelen her şeyi tek bir kutu halinde satın alabileceğiniz bir sistem üzerinde çalışıyoruz. Bir “şeyin” ticari hale gelmesinin samimiyetini baltaladığını düşündüğümden bunu karlı bir iş modeli olarak değil, projenin dünyasına insanların daha kolay girebilmesini sağlayacak bir yöntem olarak görüyorum. Zaten bu yüzden hem satışı yapılırken hem de tüm kaynakları ücretsiz paylaşılacak. Hapiçe için de bu paket haline getirme çalışması sürüyor. Tamamlandığında yazımızın bu paragrafını güncellemek için sabırsızlanıyorum.

“A’dan Z’ye başkasının tasarladığı bir projenin bana ne faydası var” diye düşünenlerdenseniz (açıkçası yıllar önce ben de öyle düşünüyordum) yanılıyor olabilirsiniz. Her ne kadar kapsamlı bir rehber sunsam da mutlaka projeyi yaparken sorunlarla karşılaşacaksınız. Belki bilgisayarınız mikrodenetleyiciyi tanımayacak, belki saatlerce yanlış bağladığınız bir kabloyu arayacaksınız. İlk bakışta bu durum olumsuz gibi görünse de aslında mantığı kavramanızın en pratik ve öğretici yolu bu. Bir “boomer” gibi görünmek istemem ama “bizim zamanımızda” (bu kelimeleri sarf etmeye alışamadım ama ne yazık ki zaman fazla hızlı değişiyor) çıkan problemleri çözmek çok daha zordu. Şimdi sorunları çözmek için güzel bir araca sahipsiniz: AI. Alıştık bu lafa ama gerçekten bu özellikle elektronik öğrenmeye çalışanlar için büyük bir lüks.

Neyse, daha fazlasını videolarda konuşuruz. Hadi şu cihazın yapımına geçelim. 😋

İlk olarak şu parçaları edinmeniz gerekiyor. Alttaki liste ögelerine tıkladığınızda sistem sizi gereken parçanın Robo90 satış sayfasına yönlendiriyor. Parça stokta yoksa veya başka bir yerden satın almak isterseniz ürün özelliklerini not alarak bunu yapabilirsiniz.

Kullanılan Ürünler:

Şimdi sırada 3d baskı işi var. Aşağıdaki linkteki .zip dosyasında basılması gereken tüm parçalar mevcut.

Artık hazırız. 🥳

Şimdi sıra bağlantıları yapmakta. Elinizdeki kartların üzerinde jumper kabloları bağlayabilmeniz için ayrılmış çıkıntılar ve delikler var. Bunların her birine “pin” diyoruz. Her pinin altında veya üstünde numarası yazar. Şimdiki göreviniz aşağıdaki listeye göre kablo bağlantılarını yapmak. Örneğin “Esp32’nin 5 numaralı pininin ekranın CS pinine bağla” ifadesi bu pinleri birbirine uygun bir jumper kablo bağlamayı ifade eder.

Sorun şu ki bazen kartlardaki pin sayısı proje için yetersiz kalır. Örneğin VCC ce GND pinleri. Elinize herhangi bir pil alırsanız bir tarafında “+” diğer tarafında “-” yazdığını görürsünüz. Devre kartlarında VCC, 5V, 3.3V, VIN gibi “V” ile başlayan ifadeler pillerdeki “+”yı, GND ise “-“yi ifade eder. Her parça temel olarak bu artı eksi beslemesine ihtiyaç duyduğundan Bu projedeki elektriği dağıtan ve sistemin beyni olarak çalışan kartın üzerindeki pinler yeterli değil. Bir pine birden fazla kablo bağlamanın pek çok yolu var elbette. Biz bu progede mini breadboard ile 1 pini çok pine çevireceğiz. Evinizdeki üçlü prizlerin çoklu versiyonu gibi düşünün. Breadboard kullanımını bir yazı ile anlatmak zor ama neyse ki Robo90 bu konuda kapsamlı bir video yapmış. vaktiniz varsa videonun tamamını izlemeniz çok faydalı olur ama vakit kısıtlıysa linkteki videonun 12:30 – 14:10 dakikaları arasını izlemeniz temel düzeyde kavramanız için yeterli olur.

Özetle bağlantıyı yaparken Esp32’nin VIN pinini ve GND pinini jumper kablo ile breadboard’a taşımalısınız. Bu kabloları aynı yere bağlamamaya özen göstermelisiniz. Eğer VIN ve GND pinlerini birbirine temas ettirirseniz ESP32’niz bozulabilir. Listede pek çok GND, VIN bağlantısı göreceksiniz. Breadboard üzerinde 5’li bloklardan birine ESPnin VCC pinini bağladığınızda 4 tane boş bağlantı yeri kalır. bazen bu bile yetmez. Bu durumda bir erkek-erkek jumper kabloyla blok deliklerden birinden aldığınız enerjiyi bir yanındaki bloğa bağlayarak pin sayısını arttırabilirsiniz. (Bence breadboard videosunun tamamını izleyin. Yazı ile anlatmakta zorlandığım tek kısım bu sanırım. Belki yakın gelecekte bu rehberi yazılı değil videolu yaparız. Sanki öyle daha iyi olacak.)

Bağlantı Şeması:

EKRAN

ESP32		EKRAN

GND		GND
VIN		VCC
18		SCK
23		SDA
22		RES
21		A0
5		CS
VIN		LED

MOTOR

ESP32		ULN2003

13		IN1
12		IN2
14		IN3
27		IN4
GND		-
VIN		+ (5V)

BUTON


ESP32		BUTON

32		PIN1
GND		PIN2

BUZZER


ESP32		HOPARLÖR

26		+
GND		-

Gelelim güç bağlantısına. tasarımda bir dişi type-C soketi kullandım aslında. Zamanında çinli sitelerden bolca aldığım için elimde vardı. Doğrusu bu projeyi yaparken bu kadar kişinin ilgisini çekeceğini beklemiyordum, o yüzden elimdekileri kullandım. Yakında tasarımı ekstra bir sokete gerek kalmayacak şekilde revize edeceğim ama bu halde olması da sorun değil. Sadece bir MicroUSB kablosuna ihtiyacınız var. ESP32ye takacağınız bu kablo ile tüm cihazı besleyebilirsiniz.

Bağlantıları bitirdiyseniz işin zor kısmı bitti demektir. 3d baskı alt kutusunun içerisinde her parçayı monte edebilmeniz için bariz kısımlar tasarladım. Bu kısımlara elektronik parçalarımızı monte ediyoruz. Montaj sırasında birkaç minik vidalara ihtiyacınız olacak. Ne yazık ki şu aşamada bu minik vidaları 3-5 tane satın alabileceğiniz online bir yer bilmiyorum. Ben zamanında 1000’li olarak aşağıdaki linkten satın almıştım. Bir set olarak satışa sunduğumuzda vida ekleriz içine ama şimdilik etrafınızdan bulduğunuz vidaları kullanmanızı önermek durumundayım.

Vidayı aldığım link: https://www.sywoods.com.tr/mini-vida-22×65-mm-1000-adet-sari

Sadece kodu yükleme kısmı kaldı. Bunun için yalnızca bir bilgisayar yeterli.

Önce ücretsiz Arduino IDE programını yüklemeniz gerekiyor. Aşağıdaki linki kullanabilirsiniz.

https://www.arduino.cc/en/software

Şimdi sayfanın sonuna eklediğim o uzunca kodu ESP32’ye nasıl aktaracağımıza geçelim. Hiç kodlama bilmiyor olabilirsiniz, merak etmeyin, adım adım ilerleyeceğiz. Sadece bir bilgisayara ve bir Micro USB kabloya ihtiyacımız var.

1. ESP32’yi Bilgisayara Bağlamak Micro USB kablonuzun bir ucunu ESP32’ye, diğer ucunu bilgisayarınıza takın. Burada çok önemli bir detay var: Çekmecede bulduğunuz eski bir kabloyu kullanıyorsanız cihaz çalışmayabilir. Kablonuzun sadece “şarj” eden değil, “veri aktarımı” yapan bir kablo olduğundan emin olun. Taktığınızda bilgisayardan ufak bir “donanım bağlandı” sesi duymalısınız. A101’lerde satılan Go-Mobile markalı kabloları kullanıyorum bu işler için ben(reklam değildir). Hem ucuzlar hem de iş görüyorlar.

2. Arduino IDE’ye ESP32’yi Tanıtmak Arduino IDE’yi ilk kurduğumuzda program sadece standart Arduino kartlarını tanır. Ona ESP32’yi de öğretmemiz gerekiyor.

  • Arduino IDE programını açın.
  • Üst menüden Dosya (File) > Tercihler (Preferences) sekmesine tıklayın.
  • Açılan pencerenin alt kısımlarında “Ek Devre Kartları Yöneticisi URL’leri” (Additional Boards Manager URLs) yazan bir boşluk göreceksiniz. Oraya şu linki kopyalayıp yapıştırın ve “Tamam” deyin:
https://dl.espressif.com/dl/package_esp32_index.json
  • Şimdi sol menüdeki ikonlardan (veya Araçlar > Kart > Kart Yöneticisi yolunu izleyerek) Kart Yöneticisi (Boards Manager) bölümüne girin.
  • Arama kutusuna “ESP32” yazın. Çıkan sonuçlarda “esp32 by Espressif Systems” paketini bulup Kur (Install) butonuna basın. Dosya boyutu biraz büyük, internet hızınıza göre birkaç dakika sürebilir, arkanıza yaslanıp bekleyin.

3. Gerekli Kütüphaneleri Yüklemek Projelerimizde ekran veya Telegram botu gibi karmaşık özellikleri kolayca kullanabilmek için başkalarının önceden yazıp paylaştığı “Kütüphane” (Library) paketlerini kullanırız. Bunları kurmak tek tıklamalık iş:

  • Sol menüdeki kitaplık ikonuna tıklayarak Kütüphane Yöneticisi (Library Manager) bölümünü açın.
  • Arama kutusuna sırasıyla aşağıdaki isimleri yazıp çıkan doğru sonuçlarda Kur (Install) butonuna tıklayın:
    1. UniversalTelegramBot (Brian Lough yazan paketi kurun. Kurarken başka paketler de kurulsun mu diye sorarsa “Install all” deyin.)
    2. Adafruit GFX Library
    3. Adafruit ST7735 and ST7789 Library
    4. ArduinoJson (Benoit Blanchon yazan paket)

4. Kodu Yapıştırmak ve Kendi Ayarlarınızı Girmek

  • Arduino IDE’nin ortasındaki beyaz sayfada var olan her şeyi tamamen silin.
  • Yukarıda paylaştığım kod bloğunun tamamını kopyalayıp bu tertemiz sayfaya yapıştırın.
  • Kodun en üst kısımlarında “KULLANICI AYARLARI” adında bir bölüm göreceksiniz. Sadece o bölümdeki XXXXXXXXXX yazan yerleri kendi bilgilerinizle değiştirmeniz gerekiyor.
  • Kendi WiFi ağınızın adını, WiFi şifrenizi, Telegram’dan aldığınız Bot Token ve Chat ID numaranızı ilgili yerlere girin. (Küçük ama kritik bir uyarı: Bilgileri girerken kelimelerin başındaki ve sonundaki çift tırnak (“) işaretlerini yanlışlıkla silmemeye dikkat etmelisiniz.)

5. Telegram Bot Token ve Chat ID Nasıl Alınır?

Kodun içindeki o XXXXXXXXXX kısımlarını doldurabilmek için Telegram üzerinde kendimize özel bir bot yaratmamız ve kendi kullanıcı numaramızı (Chat ID) öğrenmemiz gerekiyor. Gözünüz korkmasın, tamamen ücretsiz ve sadece 2 dakika sürüyor:

  • Bot Token Almak (BotFather):
    1. Telegram uygulamasını açın ve arama çubuğuna @BotFather yazın (yanında mavi tik olan resmi hesaba girin).
    2. Sohbeti başlatmak için Başlat (Start) butonuna basın veya /start yazıp gönderin.
    3. Yeni bir bot oluşturmak için /newbot komutunu gönderin.
    4. BotFather sizden botunuza bir isim vermenizi isteyecektir. İstediğiniz bir ismi yazın (Örn: HapiceHatirlatici).
    5. Şimdi de botunuz için benzersiz bir kullanıcı adı (username) belirlemenizi isteyecektir. Tek şart, bu kullanıcı adının sonunun mutlaka bot kelimesiyle bitmesi gerekir (Örn: hapice_takip_bot).
    6. Kullanıcı adını da onayladıktan sonra BotFather size uzun, karmaşık harf ve sayılardan oluşan bir kod verecektir. İşte bu kod sizin Bot Token numaranızdır. Bunu kopyalayıp koddaki #define BOTtoken satırındaki tırnakların içine yapıştırın.
  • Chat ID Almak (Kendi ID’nizi Öğrenmek):
    1. Telegram arama çubuğuna bu kez @userinfobot (veya @MyChatID_bot) yazın ve bota girip Başlat (Start) deyin.
    2. Bot size anında profil bilgilerinizi ve “Id:” başlığı altında sadece sayılardan oluşan bir numara verecektir.
    3. Bu sayısal numara sizin Chat ID‘nizdir. Bunu da kopyalayıp koddaki #define CHAT_ID satırındaki tırnakların içine yapıştırın.
  • Çok Önemli Son Adım: Az önce BotFather’ın size verdiği mesajdaki bot linkine tıklayarak kendi oluşturduğunuz botun sohbet sayfasına gidin ve Başlat (Start) butonuna basın. Botunuza ilk mesajı siz atmazsanız, sistem güvenlik nedeniyle cihazın size Telegram üzerinden mesaj göndermesine izin vermez.

6. Doğru Kartı Seçip Kodu Yüklemek Veee finale geldik!

  • Üst menüden Araçlar (Tools) kısmına tıklayın.
  • Kart (Board) menüsünün üzerine gelin, oradan esp32‘yi seçip yana açılan listeden DOIT ESP32 DEVKIT V1 modelini seçin. (Piyasada en yaygın bulunan model budur, sizdeki farklı bile olsa genelde bu seçenek sorunsuz çalışır).
  • Yine Araçlar menüsünden bu kez Port kısmına gelin. Orada COM3, COM4 (veya Mac kullanıyorsanız /dev/cu... gibi) bir seçenek göreceksiniz. Onu işaretleyin. Eğer Port kısmı griyse ve hiçbir şey seçilemiyorsa, bilgisayar kartı görmemiş demektir; büyük ihtimalle kablonuz veri aktarmıyordur, başka bir kabloyla tekrar deneyin.
  • Her şey tamsa, programın sol üst köşesindeki yuvarlak içindeki sağa bakan Yükle (Upload) okuna tıklayın.
  • Alt kısımdaki siyah pencerede yazılar akmaya başlayacak. Bir süre sonra “Connecting…” (Bağlanıyor…) yazısını ve yan yana noktaları göreceksiniz. ESP32’lerin meşhur bir huyu vardır; bu yazıyı gördüğünüz an ESP32 kartınızın üzerindeki “BOOT” yazan minik butona parmağınızla basılı tutun.
  • Yazılar yüzde %10, %20 diye ilerlemeye başladığında parmağınızı çekebilirsiniz.
  • Altta “Done uploading” (Yükleme tamamlandı) yazısını gördüğünüzde başardınız demektir! 🥳

Başta da söylediğim gibi, tüm bunları yaparken sorunlar çıkabilir. Eğer bir yerde takılırsanız bu sayfanın linkini kopyalayıp Gemini, Chatgpt gibi bir AI asistanına vererek “bu projeyi yapmaya çalışıyorum çok bilgili değilim ve şurada sorun yaşıyorum.” gibi promptlar girerek üstesinden gelebileceğinize inanıyorum.

Kolay Gelsin! 🖐️😊

Arduino IDE Kodu:

/*
  AKILLI İLAÇ DİSPANSERİ VE TAKİP SİSTEMİ
  Bu kod, saatlik ilaç rutinini takip eden, hastaya görsel/işitsel uyarı veren,
  motor mekanizması ile ilaç veren ve hasta yakınına Telegram üzerinden bilgi veren bir sistemdir.
*/

#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <UniversalTelegramBot.h>
#include <Preferences.h>
#include <Stepper.h>
#include <Adafruit_GFX.h>
#include <Adafruit_ST7735.h>
#include <SPI.h>
#include <Fonts/FreeSansBold18pt7b.h>
#include <time.h>

// ==============================================================================
// ⬇️ KULLANICI AYARLARI (KODLAMA BİLMEYENLER SADECE BURAYI DOLDURACAK) ⬇️
// ==============================================================================
const char* WIFI_SSID = "XXXXXXXXXX";        // WiFi Ağ Adı
const char* WIFI_PASSWORD = "XXXXXXXXXX";    // WiFi Şifresi
#define BOTtoken "XXXXXXXXXX"                // Telegram Bot Token (BotFather'dan alınır)
#define CHAT_ID "XXXXXXXXXX"                 // Bildirim gidecek kişinin Telegram Chat ID'si

// Cihazın kullanılacağı ülkenin saat dilimi (Türkiye için GMT+3 -> 3 * 3600 = 10800)
const long TIMEZONE_OFFSET = 10800;          
const int DAYLIGHT_OFFSET = 0;               // Yaz saati uygulaması (Türkiye'de kalktığı için 0)
// ==============================================================================
// ⬆️ KULLANICI AYARLARI BİTİŞİ ⬆️
// ==============================================================================


// --- PIN TANIMLAMALARI ---
#define TFT_CS     5
#define TFT_RST    22 
#define TFT_DC     21 
#define TFT_MOSI   23 // SDA
#define TFT_SCLK   18 // SCK

#define MOTOR_IN1  13
#define MOTOR_IN2  12
#define MOTOR_IN3  14
#define MOTOR_IN4  27

#define BUTTON_PIN 32
#define BUZZER_PIN 26

// --- MELODİ NOTA FREKANSLARI ---
#define NOTE_C5  523
#define NOTE_D5  587
#define NOTE_E5  659
#define NOTE_F5  698
#define NOTE_G4  392
#define NOTE_G5  784
#define NOTE_A4  440
#define NOTE_A5  880
#define NOTE_B4  494

// Tetris Melodisi (Nota, Vuruş)
int Tetris[] = {
  NOTE_E5,4, NOTE_B4,8, NOTE_C5,8, NOTE_D5,4, NOTE_C5,8, NOTE_B4,8,
  NOTE_A4,4, NOTE_A4,8, NOTE_C5,8, NOTE_E5,4, NOTE_D5,8, NOTE_C5,8,
  NOTE_B4,4, NOTE_B4,8, NOTE_C5,8, NOTE_D5,4, NOTE_E5,4,
  NOTE_C5,4, NOTE_A4,4, NOTE_A4,2, 0,4,
  NOTE_D5,4, NOTE_F5,8, NOTE_A5,4, NOTE_G5,8, NOTE_F5,8,
  NOTE_E5,4, NOTE_C5,8, NOTE_E5,4, NOTE_D5,8, NOTE_C5,8,
  NOTE_B4,4, NOTE_B4,8, NOTE_C5,8, NOTE_D5,4, NOTE_E5,4,
  NOTE_C5,4, NOTE_A4,4, NOTE_A4,2, 0,4
};

// Başarı Melodisi
int basari[] = { NOTE_G4, 12, NOTE_C5, 12, NOTE_E5, 12, NOTE_G5, 4, NOTE_E5, 12, NOTE_G5, 2 };

// --- NESNELER ---
Adafruit_ST7735 tft = Adafruit_ST7735(TFT_CS, TFT_DC, TFT_RST);
Stepper stepper(2048, MOTOR_IN1, MOTOR_IN3, MOTOR_IN2, MOTOR_IN4);
WiFiClientSecure client;
UniversalTelegramBot bot(BOTtoken, client);
Preferences preferences;

// --- GLOBAL DEĞİŞKENLER (Çift Çekirdek uyumlu olması için volatile eklendi) ---
volatile unsigned long intervalSeconds = 0; // Artık saniye bazlı tutuyoruz (00:01 desteği için)
volatile unsigned long lastPillTime = 0;
volatile bool alarmActive = false;
volatile bool buzzerMuted = false;
volatile unsigned long alarmStartTime = 0;

volatile bool alert5MinSent = false;
volatile bool alert15MinSent = false;
volatile bool weeklyReportSent = false;
volatile bool forceScreenRefresh = false;

// Puanlama Sistemi
int totalDosesThisWeek = 0;
int onTimeDosesThisWeek = 0;

// Ekran tazeleme için
int sonSaat = -1;
int sonDakika = -1;
int sonSaniye = -1;
bool isFlashing = false;

// Kesme (Interrupt) için bayrak
volatile bool buttonPressed = false;

// --- TELEGRAM MESAJ KUYRUĞU (Çekirdekler arası iletişim) ---
#define MAX_MSG_QUEUE 10
String msgQueue[MAX_MSG_QUEUE];
volatile int queueHead = 0;
volatile int queueTail = 0;

void queueTelegramMessage(String msg) {
  int nextHead = (queueHead + 1) % MAX_MSG_QUEUE;
  if (nextHead != queueTail) { // Kuyruk dolu değilse
    msgQueue[queueHead] = msg;
    queueHead = nextHead;
  }
}

volatile unsigned long lastInterruptTime = 0;
void IRAM_ATTR handleButton() {
  unsigned long interruptTime = millis();
  // Buton sekmesini (bouncing) önlemek için 200 milisaniye koruma
  if (interruptTime - lastInterruptTime > 200) {
    buttonPressed = true;
  }
  lastInterruptTime = interruptTime;
}

// ==========================================
// TELEGRAM GÖREVİ (CORE 0 - Arka Plan)
// ==========================================
void telegramTaskCode(void * pvParameters) {
  for(;;) {
    // 1. Ana döngüden gelen (kuyruktaki) mesajları gönder
    while (queueTail != queueHead) {
      String msg = msgQueue[queueTail];
      bot.sendMessage(CHAT_ID, msg, "");
      queueTail = (queueTail + 1) % MAX_MSG_QUEUE;
    }
    
    // 2. Yeni mesajları kontrol et
    int numNewMessages = bot.getUpdates(bot.last_message_received + 1);
    while (numNewMessages) {
      handleTelegramMessages(numNewMessages);
      numNewMessages = bot.getUpdates(bot.last_message_received + 1);
    }
    
    // İşlemciyi ve WiFi'yi boğmamak için bekle
    vTaskDelay(2000 / portTICK_PERIOD_MS); 
  }
}

void setup() {
  Serial.begin(115200);
  
  pinMode(BUZZER_PIN, OUTPUT);
  pinMode(BUTTON_PIN, INPUT_PULLUP);
  attachInterrupt(BUTTON_PIN, handleButton, FALLING); 
  
  stepper.setSpeed(15); 
  
  tft.initR(INITR_144GREENTAB); 
  tft.setRotation(2);
  tft.fillScreen(ST77XX_BLACK);
  
  tft.setTextColor(ST77XX_WHITE);
  tft.setTextSize(1);
  tft.setCursor(10, 10);

  if (String(WIFI_SSID) == "XXXXXXXXXX" || String(BOTtoken) == "XXXXXXXXXX") {
    tft.setTextColor(ST77XX_RED);
    tft.println("HATA!");
    tft.println("Lutfen kod icindeki");
    tft.println("WIFI, BOT_TOKEN ve");
    tft.println("CHAT_ID ayarlarini doldurun!");
    while (true); 
  }

  // NVS Başlat
  preferences.begin("ilac_app", false);
  intervalSeconds = preferences.getULong("intSec", 0); // Eski "interval" yerine intSec kullanıldı
  lastPillTime = preferences.getULong("lastPill", 0);
  totalDosesThisWeek = preferences.getInt("totDose", 0);
  onTimeDosesThisWeek = preferences.getInt("onTDose", 0);

  tft.println("Wifi Araniyor...");
  WiFi.mode(WIFI_STA);
  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
  client.setInsecure(); 

  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
    tft.print(".");
  }
  
  tft.fillScreen(ST77XX_BLACK);
  tft.setCursor(10, 10);
  tft.setTextColor(ST77XX_GREEN);
  tft.println("Baglanti Basarili!");
  tft.setTextColor(ST77XX_WHITE);
  tft.println("Saat bilgisi aliniyor...");

  configTime(TIMEZONE_OFFSET, DAYLIGHT_OFFSET, "pool.ntp.org", "time.nist.gov");
  struct tm timeinfo;
  while (!getLocalTime(&timeinfo)) {
    Serial.println("NTP bekleniyor...");
    delay(1000);
  }
  tft.println("Saat guncellendi.");
  
  String baslangicMesaji = "🤖 Sistem Başlatıldı!\n";
  
  if (intervalSeconds == 0) {
    tft.fillScreen(ST77XX_BLACK);
    tft.setCursor(10, 50);
    tft.setTextColor(ST77XX_YELLOW);
    tft.println("Lutfen ilacin");
    tft.println("araligini");
    tft.println("Telegram'dan giriniz.");
    baslangicMesaji += "⚠️ Lütfen ilacın periyodunu giriniz. (Örn: 'aralık 8' veya 'aralık 00:01')";
  } else {
    baslangicMesaji += "✅ Süre aralığı hafızadan yüklendi.\nCihaz hazır.";
    tft.fillScreen(ST77XX_BLACK); 
    sonSaat = -1; sonDakika = -1; sonSaniye = -1; 
  }
  
  queueTelegramMessage(baslangicMesaji);

  // ESP32'nin Core 0'ında Telegram Görevini Başlat (Bu sayede ekran DONMAYACAK)
  xTaskCreatePinnedToCore(
    telegramTaskCode,
    "TelegramTask",
    10000,
    NULL,
    1,
    NULL,
    0);
}

void loop() {
  unsigned long currentUnixTime = getCurrentUnixTime();

  // Telegram üzerinden yeni veri girildiyse ekranı tazelemek için bayrak
  if (forceScreenRefresh) {
    tft.fillScreen(ST77XX_BLACK);
    sonSaat = -1; sonDakika = -1; sonSaniye = -1;
    forceScreenRefresh = false;
  }

  checkWeeklyReport();

  if (intervalSeconds == 0) return; // Ayar yapılana kadar beklet

  // SAYAÇ VE ALARM MANTIĞI
  long secondsRemaining = (lastPillTime + intervalSeconds) - currentUnixTime;

  if (secondsRemaining <= 0) {
    // ALARM DURUMU
    if (!alarmActive) {
      alarmActive = true;
      alarmStartTime = currentUnixTime;
      buzzerMuted = false;
      alert5MinSent = false;
      alert15MinSent = false;
      queueTelegramMessage("🚨 İlaç zamanı geldi! Alarm veriliyor."); // Bloklamadan gönder
      tft.fillScreen(ST77XX_BLACK);
      sonSaat = -1; sonDakika = -1; sonSaniye = -1;
    }
    
    long secondsLate = abs(secondsRemaining);
    ekraniCiz(secondsLate, true); 

    if (secondsLate >= 300 && !alert5MinSent) {
      queueTelegramMessage("⚠️ Alarm 5 dakikadır çalıyor, hasta hala ilacını almadı!");
      alert5MinSent = true;
    }
    if (secondsLate >= 900 && !alert15MinSent) {
      queueTelegramMessage("🆘 15 dakika oldu, lütfen hastaya ulaşınız!");
      alert15MinSent = true;
    }

    if (!buzzerMuted) {
      playTetrisNonBlocking(); 
    }

  } else {
    // NORMAL GERİ SAYIM
    alarmActive = false;
    ekraniCiz(secondsRemaining, false);
  }

  // BUTON KONTROLÜ
  if (buttonPressed) {
    buttonPressed = false; 
    
    // SADECE alarm aktifse (zamanı geldiyse) ilaç ver
    if (alarmActive) {
      ilacVer(currentUnixTime);
    } 
    // Zamanı gelmeden basılırsa sistem hiçbir şey yapmadan görmezden gelir.
  }
}

// ==========================================
// YARDIMCI FONKSİYONLAR
// ==========================================

void handleTelegramMessages(int numNewMessages) {
  for (int i = 0; i < numNewMessages; i++) {
    String chat_id = String(bot.messages[i].chat_id);
    if (chat_id != CHAT_ID) continue; 

    String text = bot.messages[i].text;
    text.toLowerCase();

    // Aralık algılama (Hem "aralık 8" hem "aralık 00:01" formatını destekler)
    if (text.startsWith("aralık ")) {
      String param = text.substring(7);
      param.trim();
      unsigned long yeniSaniye = 0;

      // HH:MM format kontrolü
      if (param.indexOf(':') != -1) {
        int separatorIndex = param.indexOf(':');
        int h = param.substring(0, separatorIndex).toInt();
        int m = param.substring(separatorIndex + 1).toInt();
        yeniSaniye = (h * 3600) + (m * 60);
      } else {
        // Sadece saat formatı
        yeniSaniye = param.toInt() * 3600;
      }

      if (yeniSaniye > 0) {
        intervalSeconds = yeniSaniye;
        preferences.putULong("intSec", intervalSeconds);
        
        if (lastPillTime == 0) {
           lastPillTime = getCurrentUnixTime();
           preferences.putULong("lastPill", lastPillTime);
        }
        
        forceScreenRefresh = true;
        bot.sendMessage(chat_id, "✅ Aralık ayarlandı. (" + param + ")", "");
      } else {
        bot.sendMessage(chat_id, "❌ Hatalı değer! Örnek: 'aralık 8' veya 'aralık 00:01'", "");
      }
    } 
    else if (text == "ilaç alındı" || text == "ilac alindi") {
      bot.sendMessage(chat_id, "✅ Komut alındı. Sayım sıfırlanıyor (Motor dönmeyecek).", "");
      resetTimer(getCurrentUnixTime());
    } 
    else if (text == "sustur") {
      if (alarmActive) {
        buzzerMuted = true;
        noTone(BUZZER_PIN);
        bot.sendMessage(chat_id, "🔇 Alarm susturuldu. Ekran yanıp sönmeye devam edecek.", "");
      } else {
        bot.sendMessage(chat_id, "Cihaz şu an zaten alarm durumunda değil.", "");
      }
    }
    else {
      bot.sendMessage(chat_id, "Komutlar:\n- aralık [saat] veya [saat:dakika] (Örn: aralık 8 veya aralık 00:01)\n- ilaç alındı\n- sustur", "");
    }
  }
}

void ilacVer(unsigned long currentUnixTime) {
  // 1. Gecikme süresini hesapla (Gerçek planlanan zaman ile şu anki zaman arası fark)
  long expectedPillTime = lastPillTime + intervalSeconds;
  long delaySecs = currentUnixTime - expectedPillTime;
  if (delaySecs < 0) delaySecs = 0; // Hata payı tedbiri
  
  int dHours = delaySecs / 3600;
  int dMins = (delaySecs / 60) % 60;
  int dSecs = delaySecs % 60;

  // Saniye verisini dakikaya yuvarla (30 saniye ve üzeri ise 1 dakika ekler)
  if (dSecs >= 30) {
    dMins++;
    if (dMins >= 60) {
      dMins = 0;
      dHours++;
    }
  }

  String gecikmeStr = "";
  if (dHours > 0) {
    gecikmeStr = String(dHours) + " saat " + String(dMins) + " dakika";
  } else {
    gecikmeStr = String(dMins) + " dakika";
  }

  // Puanlama Kontrolü
  totalDosesThisWeek++;
  if (delaySecs <= 900) { // İlk 15 dakika içinde alındıysa zamanında say
    onTimeDosesThisWeek++;
  }
  preferences.putInt("totDose", totalDosesThisWeek);
  preferences.putInt("onTDose", onTimeDosesThisWeek);

  noTone(BUZZER_PIN);
  
  // 2. ARA EKRAN: Gecikme Süresi (2 Saniye Bekler)
  tft.fillScreen(ST77XX_BLACK);
  tft.setFont(); // Standart okunabilir fonta dön
  tft.setTextSize(2);
  tft.setTextColor(ST77XX_YELLOW);
  tft.setCursor(16, 20);
  tft.print("Gecikme:");
  
  tft.setTextColor(ST77XX_WHITE);
  if (dHours > 0) {
    tft.setCursor(20, 50);
    tft.print(String(dHours) + " saat");
    tft.setCursor(20, 80);
    tft.print(String(dMins) + " dk");
  } else {
    tft.setCursor(10, 60);
    tft.print(String(dMins) + " dakika");
  }

  // Telegram Bildirimi (Kuyruğa atılır, donma yapmaz)
  queueTelegramMessage("💊 Hasta İlacını Aldı! Gecikme: " + gecikmeStr);

  delay(2000); // Ekrandaki gecikme yazısının okunması için 2 saniye bekle

  // 3. İLAÇ VERİLİYOR EKRANI
  tft.fillScreen(ST77XX_BLACK);
  tft.setFont(); // Standart font
  tft.setTextSize(2);
  tft.setTextColor(ST77XX_GREEN);
  tft.setCursor(16, 40);
  tft.print("Ilaciniz");
  tft.setCursor(10, 70);
  tft.print("veriliyor");
  
  // 4. Motoru Döndür
  stepper.step(147);
  
  digitalWrite(MOTOR_IN1, LOW);
  digitalWrite(MOTOR_IN2, LOW);
  digitalWrite(MOTOR_IN3, LOW);
  digitalWrite(MOTOR_IN4, LOW);

  playSuccessMelody();
  resetTimer(currentUnixTime);
  
  // İlaç verilirken gerçekleşmiş olabilecek tüm buton sekmelerini iptal et
  buttonPressed = false; 
}

void resetTimer(unsigned long currentUnixTime) {
  lastPillTime = currentUnixTime;
  preferences.putULong("lastPill", lastPillTime);
  alarmActive = false;
  buzzerMuted = false;
  alert5MinSent = false;
  alert15MinSent = false;
  forceScreenRefresh = true;
}

void playTetrisNonBlocking() {
  static int noteIndex = 0;
  static unsigned long previousMillis = 0;
  static bool isRest = false;
  static int currentDuration = 0;
  
  int totalNotes = sizeof(Tetris) / sizeof(Tetris[0]) / 2;
  int tempo = 144;
  int wholenote = (60000 * 4) / tempo;
  
  // O anki nota veya boşluk süresi doldu mu kontrol et
  if (millis() - previousMillis >= currentDuration) {
    previousMillis = millis(); // Zamanı sıfırla
    
    if (!isRest) {
      if (noteIndex >= totalNotes) noteIndex = 0;

      int noteVal = Tetris[noteIndex * 2];
      int divider = Tetris[(noteIndex * 2) + 1];

      if (divider > 0) {
        currentDuration = (wholenote) / divider;
      } else if (divider < 0) {
        currentDuration = (wholenote) / abs(divider);
        currentDuration *= 1.5;
      }

      if (noteVal != 0) tone(BUZZER_PIN, noteVal, currentDuration * 0.9);
      else noTone(BUZZER_PIN);
      
      isRest = true; // Sırada kısa bir boşluk var
    } else {
      noTone(BUZZER_PIN);
      currentDuration = 20; // Notalar arası staccato (kesik çalma) için 20ms boşluk
      isRest = false;
      noteIndex++;
    }
  }
}

void playSuccessMelody() {
  int tempo = 120;
  int wholenote = (60000 * 4) / tempo;
  for (int i = 0; i < sizeof(basari) / sizeof(basari[0]) / 2; i++) {
    int divider = basari[i * 2 + 1];
    int noteDuration = (wholenote) / divider;
    tone(BUZZER_PIN, basari[i * 2], noteDuration * 0.9);
    delay(noteDuration);
    noTone(BUZZER_PIN);
  }
}

void ekraniCiz(long saniyeVerisi, bool isAlarm) {
  int saat = (saniyeVerisi / 3600);
  int dakika = (saniyeVerisi / 60) % 60;
  int saniye = saniyeVerisi % 60;

  char metinKutusu[15];

  if (isAlarm) {
    // ALARM EKRANI TASARIMI (Rengarenk + İLAÇ ZAMANI + 00:00:00)
    uint16_t renkler[] = {ST77XX_RED, ST77XX_BLUE, ST77XX_MAGENTA, ST77XX_YELLOW, ST77XX_GREEN};
    int renkIndeksi = (millis() / 300) % 5; // Her 300ms'de bir renk değişir
    uint16_t bgColor = renkler[renkIndeksi];
    
    static int sonRenkIndeksi = -1;

    // Sadece renk değiştiğinde veya saniye arttığında ekranı tazele (Titremeyi engeller)
    if (sonRenkIndeksi != renkIndeksi || saniye != sonSaniye || !isFlashing) {
      tft.fillScreen(bgColor);
      
      tft.setFont(); // Standart temiz pixel fonta geç
      tft.setTextSize(2);
      tft.setTextColor(ST77XX_BLACK); // <-- YAZI RENGİ SİYAH OLARAK GÜNCELLENDİ
      
      // Ekrana ortalamak için manuel koordinatlar (128x128 ekran için)
      tft.setCursor(40, 15);
      tft.print("ILAC");
      tft.setCursor(28, 35);
      tft.print("ZAMANI");

      // Gecikme etiketi
      tft.setTextSize(1);
      tft.setCursor(43, 70);
      tft.print("Gecikme");

      // 00:00:00 Gecikme sayacı
      sprintf(metinKutusu, "%02d:%02d:%02d", saat, dakika, saniye);
      tft.setTextSize(2);
      tft.setCursor(16, 85); // Tam ortaya hizalandı
      tft.print(metinKutusu);

      sonRenkIndeksi = renkIndeksi;
      sonSaat = saat;
      sonDakika = dakika;
      sonSaniye = saniye;
      isFlashing = true; // Alarm döngüsünde olduğumuzu işaretle
    }
  } else {
    // NORMAL GERİ SAYIM EKRANI TASARIMI
    if (isFlashing) { 
      tft.fillScreen(ST77XX_BLACK); // Alarm bittiyse ekranı siyah yap
      sonSaat = -1; sonDakika = -1; sonSaniye = -1;
      isFlashing = false;
    }

    uint16_t bgColor = ST77XX_BLACK;

    if (saat != sonSaat) {
      sprintf(metinKutusu, "%02d", saat);
      tft.fillRect(25, 10, 60, 32, bgColor); 
      tft.setFont(&FreeSansBold18pt7b);
      tft.setTextSize(1); 
      tft.setCursor(30, 38);
      tft.setTextColor(ST77XX_CYAN);
      tft.print(metinKutusu);

      tft.setFont(); 
      tft.setTextSize(1);
      tft.setCursor(90, 25);
      tft.setTextColor(ST77XX_WHITE);
      tft.print("SAAT");
      sonSaat = saat;
    }

    if (dakika != sonDakika) {
      sprintf(metinKutusu, "%02d", dakika);
      tft.fillRect(25, 50, 60, 32, bgColor);
      tft.setFont(&FreeSansBold18pt7b);
      tft.setCursor(30, 78);
      tft.setTextColor(ST77XX_GREEN);
      tft.print(metinKutusu);

      tft.setFont();
      tft.setCursor(90, 65);
      tft.setTextColor(ST77XX_WHITE);
      tft.print("DAK");
      sonDakika = dakika;
    }

    if (saniye != sonSaniye) {
      sprintf(metinKutusu, "%02d", saniye);
      tft.fillRect(25, 90, 60, 32, bgColor);
      tft.setFont(&FreeSansBold18pt7b);
      tft.setCursor(30, 118);
      tft.setTextColor(ST77XX_YELLOW);
      tft.print(metinKutusu);

      tft.setFont();
      tft.setCursor(90, 105);
      tft.setTextColor(ST77XX_WHITE);
      tft.print("SAN");
      sonSaniye = saniye;
    }
  }
}

unsigned long getCurrentUnixTime() {
  time_t now;
  time(&now);
  return now;
}

void checkWeeklyReport() {
  struct tm timeinfo;
  if (!getLocalTime(&timeinfo)) return;

  if (timeinfo.tm_wday == 0 && timeinfo.tm_hour == 21 && timeinfo.tm_min == 0) {
    if (!weeklyReportSent) {
      
      int score = 100; 
      if (totalDosesThisWeek > 0) {
        score = (onTimeDosesThisWeek * 100) / totalDosesThisWeek;
      }

      String report = "📊 HAFTALIK İLAÇ RAPORU 📊\n\n";
      report += "Bu hafta alınması gereken: " + String(totalDosesThisWeek) + "\n";
      report += "Zamanında alınan: " + String(onTimeDosesThisWeek) + "\n";
      report += "\n💯 HAFTALIK PUAN: " + String(score) + "/100\n";
      
      if(score == 100) report += "Mükemmel! Bütün ilaçlar zamanında alındı.";
      else if(score > 70) report += "İyi ama biraz daha dikkat edilebilir.";
      else report += "Lütfen hastanın ilaç rutinini sıkı takibe alınız!";

      queueTelegramMessage(report);

      totalDosesThisWeek = 0;
      onTimeDosesThisWeek = 0;
      preferences.putInt("totDose", 0);
      preferences.putInt("onTDose", 0);

      weeklyReportSent = true;
    }
  } else {
    weeklyReportSent = false;
  }
}