#include <SPI.h>
#include <Adafruit_GFX.h>
#include <Adafruit_ILI9341.h>
// TFT display pins
#define TFT_CS 15
#define TFT_DC 2
#define TFT_RST 4
Adafruit_ILI9341 tft = Adafruit_ILI9341(TFT_CS, TFT_DC, TFT_RST);
// Joystick
const int joyX = 34;
const int joyY = 35;
// Buttons (active LOW)
const int btnConfirm = 32;
const int btnBack = 33;
const int btnExtra = 25;
// Buzzer
const int buzzerPin = 26;
// Debounce variables
bool lastConfirmState = HIGH;
bool lastBackState = HIGH;
bool lastExtraState = HIGH;
unsigned long lastConfirmTime = 0, lastBackTime = 0, lastExtraTime = 0;
const unsigned long debounceDelay = 200;
// Menu control
int menuIndex = 0;
const int menuLength = 2;
bool inSubMenu = false;
// Screen update
bool screenNeedsUpdate = true;
// Breathing Exercise variables
bool breathingExerciseActive = false;
unsigned long breathingStartTime = 0;
const unsigned long breathingDuration = 30000;
const unsigned long inhaleTime = 4000;
const unsigned long holdTime = 7000;
const unsigned long exhaleTime = 8000;
const unsigned long totalCycle = inhaleTime + holdTime + exhaleTime;
const int minRadius = 20;
const int maxRadius = 80;
const int circleX = 160;
const int circleY = 120;
// Stopwatch buffer
char stopwatchBuffer[9];
// Time Management variables
bool timeManagementActive = false;
// Example static task list (max 5 tasks)
const char* tasks[] = {
"1. Buy groceries",
"2. Finish project",
"3. Call John",
"4. Read book",
"5. Workout"
};
const int taskCount = 5;
// Current year and month for calendar display
int currentYear = 2025;
int currentMonth = 6; // June
// Joystick navigation timing
const int deadZone = 500, centerVal = 2048;
unsigned long lastNavTime = 0;
const unsigned long navDelay = 300;
// For time management screen refresh control
unsigned long lastTimeMgmtRefresh = 0;
const unsigned long timeMgmtRefreshInterval = 1000; // 1 second refresh interval
void setup() {
Serial.begin(115200);
tft.begin();
tft.setRotation(1);
tft.fillScreen(ILI9341_BLACK);
pinMode(btnConfirm, INPUT_PULLUP);
pinMode(btnBack, INPUT_PULLUP);
pinMode(btnExtra, INPUT_PULLUP);
pinMode(buzzerPin, OUTPUT);
}
void loop() {
if (breathingExerciseActive) {
runBreathingExercise();
handleBackDuringBreathing();
} else if (timeManagementActive) {
runTimeManagement();
handleBackDuringTimeManagement();
} else {
handleJoystick();
handleConfirmButton();
handleBackButton();
handleExtraButton();
if (screenNeedsUpdate) {
updateDisplay();
screenNeedsUpdate = false;
}
}
}
// ------------------- INPUT HANDLERS -------------------
void handleJoystick() {
int yVal = analogRead(joyY);
if (millis() - lastNavTime > navDelay && !inSubMenu) {
if (yVal < centerVal - deadZone && menuIndex > 0) {
menuIndex--;
screenNeedsUpdate = true;
lastNavTime = millis();
} else if (yVal > centerVal + deadZone && menuIndex < menuLength - 1) {
menuIndex++;
screenNeedsUpdate = true;
lastNavTime = millis();
}
}
}
void handleConfirmButton() {
bool reading = digitalRead(btnConfirm);
if (reading != lastConfirmState && millis() - lastConfirmTime > debounceDelay) {
lastConfirmTime = millis();
if (reading == LOW) {
if (!inSubMenu) {
inSubMenu = true;
} else {
if (menuIndex == 0) startBreathingExercise();
else if (menuIndex == 1) startTimeManagement();
}
screenNeedsUpdate = true;
}
}
lastConfirmState = reading;
}
void handleBackButton() {
bool reading = digitalRead(btnBack);
if (reading != lastBackState && millis() - lastBackTime > debounceDelay) {
lastBackTime = millis();
if (reading == LOW) {
if (inSubMenu) {
// If in submenu but no active feature, just go back to menu
if (!breathingExerciseActive && !timeManagementActive) {
inSubMenu = false;
screenNeedsUpdate = true;
}
}
}
}
lastBackState = reading;
}
void handleExtraButton() {
bool reading = digitalRead(btnExtra);
if (reading != lastExtraState && millis() - lastExtraTime > debounceDelay) {
lastExtraTime = millis();
if (reading == LOW) {
// Placeholder for future features
}
}
lastExtraState = reading;
}
void handleBackDuringBreathing() {
bool reading = digitalRead(btnBack);
if (reading != lastBackState && millis() - lastBackTime > debounceDelay) {
lastBackTime = millis();
if (reading == LOW) {
breathingExerciseActive = false;
inSubMenu = false;
screenNeedsUpdate = true;
}
}
lastBackState = reading;
}
void handleBackDuringTimeManagement() {
bool reading = digitalRead(btnBack);
if (reading != lastBackState && millis() - lastBackTime > debounceDelay) {
lastBackTime = millis();
if (reading == LOW) {
timeManagementActive = false;
inSubMenu = false;
screenNeedsUpdate = true;
}
}
lastBackState = reading;
}
// ------------------- MENU DISPLAY -------------------
void updateDisplay() {
tft.fillScreen(ILI9341_BLACK);
tft.setTextSize(2);
tft.setCursor(10, 10);
tft.setTextColor(ILI9341_WHITE);
if (!inSubMenu) {
tft.println("Menu:");
drawOption(0, "Breathing Exercise", menuIndex == 0);
drawOption(1, "Time Management", menuIndex == 1);
} else {
tft.println("Selected:");
tft.setCursor(10, 50);
tft.setTextColor(ILI9341_GREEN);
if (menuIndex == 0) {
tft.println("Breathing Exercise");
tft.setCursor(10, 90);
tft.setTextColor(ILI9341_WHITE);
tft.println("Press Confirm to start");
} else {
tft.println("Time Management");
tft.setCursor(10, 90);
tft.setTextColor(ILI9341_WHITE);
tft.println("Press Confirm to open");
}
}
}
void drawOption(int index, const char* label, bool selected) {
int y = 40 + index * 30;
tft.setCursor(10, y);
tft.setTextColor(selected ? ILI9341_YELLOW : ILI9341_WHITE);
tft.print(selected ? "> " : " ");
tft.println(label);
}
// ------------------- BREATHING EXERCISE -------------------
void startBreathingExercise() {
breathingExerciseActive = true;
breathingStartTime = millis();
tft.fillScreen(ILI9341_BLACK);
}
void runBreathingExercise() {
unsigned long now = millis();
unsigned long elapsed = now - breathingStartTime;
if (elapsed >= breathingDuration) {
breathingExerciseActive = false;
inSubMenu = false;
screenNeedsUpdate = true;
return;
}
unsigned long seconds = elapsed / 1000;
sprintf(stopwatchBuffer, "%02lu:%02lu", seconds / 60, seconds % 60);
unsigned long cycleTime = elapsed % totalCycle;
int radius = minRadius;
if (cycleTime < inhaleTime) {
float progress = 1.0f - (float)cycleTime / inhaleTime;
radius = minRadius + (maxRadius - minRadius) * progress;
if (cycleTime < 50) tone(buzzerPin, 1000, 100);
} else if (cycleTime < inhaleTime + holdTime) {
radius = minRadius;
} else {
float progress = (float)(cycleTime - inhaleTime - holdTime) / exhaleTime;
radius = minRadius + (maxRadius - minRadius) * progress;
if (cycleTime < inhaleTime + holdTime + 50) tone(buzzerPin, 1000, 100);
}
// Only redraw circle and text to reduce flicker
tft.fillRect(0, 0, 320, 240, ILI9341_BLACK);
tft.fillCircle(circleX, circleY, radius, ILI9341_BLUE);
const char* phaseText;
if (cycleTime < inhaleTime) phaseText = "Inhale";
else if (cycleTime < inhaleTime + holdTime) phaseText = "Hold";
else phaseText = "Exhale";
tft.setTextSize(3);
tft.setTextColor(ILI9341_WHITE);
int16_t x1, y1;
uint16_t w, h;
tft.getTextBounds(phaseText, 0, 0, &x1, &y1, &w, &h);
tft.setCursor((320 - w) / 2, circleY - radius - h - 10);
tft.print(phaseText);
tft.setTextSize(2);
tft.setTextColor(ILI9341_GREEN);
tft.getTextBounds(stopwatchBuffer, 0, 0, &x1, &y1, &w, &h);
tft.setCursor((320 - w) / 2, circleY + radius + 10);
tft.print(stopwatchBuffer);
delay(20);
}
// ------------------- TIME MANAGEMENT -------------------
void startTimeManagement() {
timeManagementActive = true;
lastTimeMgmtRefresh = 0; // force immediate redraw
tft.fillScreen(ILI9341_BLACK); // Clear entire screen once
drawCalendarAndTasks();
}
void runTimeManagement() {
if (millis() - lastTimeMgmtRefresh > timeMgmtRefreshInterval) {
drawCalendarAndTasks();
lastTimeMgmtRefresh = millis();
}
}
void drawCalendarAndTasks() {
// Instead of clearing the entire screen, clear only calendar and task areas
// Clear calendar area
int calWidth = 224;
int calHeight = 240;
tft.fillRect(0, 0, calWidth, calHeight, ILI9341_BLACK);
// Clear task list area
int taskX = calWidth + 1;
int taskWidth = 96;
tft.fillRect(taskX, 0, taskWidth, calHeight, ILI9341_BLACK);
drawCalendar(0, 0, calWidth, calHeight, currentYear, currentMonth);
drawTaskList(taskX, 0, taskWidth, calHeight);
}
void drawCalendar(int x, int y, int w, int h, int year, int month) {
tft.drawRect(x, y, w, h, ILI9341_WHITE);
// Month title
tft.setTextSize(2);
tft.setTextColor(ILI9341_YELLOW);
tft.setCursor(x + 10, y + 5);
const char* monthNames[12] = {"Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"};
char header[20];
sprintf(header, "%s %d", monthNames[month-1], year);
tft.print(header);
// Weekday names individually aligned centered in each cell
const char* weekdays[7] = {"Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"};
int cellW = 30;
int startX = x + 10;
int headerY = y + 30;
tft.setTextSize(1);
tft.setTextColor(ILI9341_WHITE);
for (int i = 0; i < 7; i++) {
int textX = startX + i * cellW + 9; // center approx (30/2 - 3 char width/2)
tft.setCursor(textX, headerY);
tft.print(weekdays[i]);
}
// Days grid
int startY = y + 45;
int cellH = 24;
int firstDay = dayOfWeek(year, month, 1); // Sunday=0
int daysInMonth = getDaysInMonth(year, month);
int day = 1;
for (int row = 0; row < 6; row++) {
for (int col = 0; col < 7; col++) {
int posX = startX + col * cellW;
int posY = startY + row * cellH;
if (row == 0 && col < firstDay) {
// Empty cells before first day
continue;
}
if (day <= daysInMonth) {
// Highlight day 4 as red
if (day == 4) tft.setTextColor(ILI9341_RED);
else tft.setTextColor(ILI9341_WHITE);
// Center day numbers in cells by adding horizontal offset
int textX = posX + 10;
tft.setCursor(textX, posY);
if(day < 10) tft.print(' '); // Padding for single-digit numbers
tft.print(day);
day++;
}
}
}
}
void drawTaskList(int x, int y, int w, int h) {
tft.drawRect(x, y, w, h, ILI9341_WHITE);
tft.setTextSize(2);
tft.setTextColor(ILI9341_CYAN);
tft.setCursor(x + 5, y + 5);
tft.println("Tasks:");
tft.setTextSize(1);
tft.setTextColor(ILI9341_WHITE);
int startY = y + 30;
for (int i = 0; i < taskCount; i++) {
tft.setCursor(x + 5, startY + i * 20);
tft.println(tasks[i]);
}
}
// Utility functions:
int dayOfWeek(int year, int month, int day) {
// Zeller's Congruence (0=Saturday, so shift)
if (month < 3) {
month += 12;
year--;
}
int K = year % 100;
int J = year / 100;
int h = (day + 13*(month+1)/5 + K + K/4 + J/4 + 5*J) % 7;
// Convert: 0=Saturday to 0=Sunday
int d = (h + 6) % 7;
return d;
}
int getDaysInMonth(int year, int month) {
if (month == 2) {
// Leap year check
bool leap = (year % 4 == 0 && (year % 100 != 0 || year % 400 == 0));
return leap ? 29 : 28;
}
if (month == 4 || month == 6 || month == 9 || month == 11) return 30;
return 31;
}