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:
UniversalTelegramBot(Brian Lough yazan paketi kurun. Kurarken başka paketler de kurulsun mu diye sorarsa “Install all” deyin.)Adafruit GFX LibraryAdafruit ST7735 and ST7789 LibraryArduinoJson(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
XXXXXXXXXXyazan 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):
- Telegram uygulamasını açın ve arama çubuğuna
@BotFatheryazın (yanında mavi tik olan resmi hesaba girin). - Sohbeti başlatmak için Başlat (Start) butonuna basın veya
/startyazıp gönderin. - Yeni bir bot oluşturmak için
/newbotkomutunu gönderin. - BotFather sizden botunuza bir isim vermenizi isteyecektir. İstediğiniz bir ismi yazın (Örn: HapiceHatirlatici).
- Şimdi de botunuz için benzersiz bir kullanıcı adı (username) belirlemenizi isteyecektir. Tek şart, bu kullanıcı adının sonunun mutlaka
botkelimesiyle bitmesi gerekir (Örn: hapice_takip_bot). - 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 BOTtokensatırındaki tırnakların içine yapıştırın.
- Telegram uygulamasını açın ve arama çubuğuna
- Chat ID Almak (Kendi ID’nizi Öğrenmek):
- Telegram arama çubuğuna bu kez
@userinfobot(veya@MyChatID_bot) yazın ve bota girip Başlat (Start) deyin. - Bot size anında profil bilgilerinizi ve “Id:” başlığı altında sadece sayılardan oluşan bir numara verecektir.
- Bu sayısal numara sizin Chat ID‘nizdir. Bunu da kopyalayıp koddaki
#define CHAT_IDsatırındaki tırnakların içine yapıştırın.
- Telegram arama çubuğuna bu kez
- Ç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;
}
}

