// Uncomment the next line to enable BLE support
#define ENABLE_BLE
#include <Arduino.h>
#include <TFT_eSPI.h>
#include <Wire.h>
#include <SPI.h>
#include <WiFi.h>
#include <ESP32Time.h>
#ifdef ENABLE_BLE
#include <BleSerial.h>
BleSerial ble;
#endif
// Display Driver
TFT_eSPI tft = TFT_eSPI();
// Touch Controller (CST816T)
#define TOUCH_SDA 6
#define TOUCH_SCL 7
#define TOUCH_INT 5
#define TOUCH_RST 4
// Display Pins (ST7789V2)
#define TFT_MOSI 11
#define TFT_SCLK 12
#define TFT_CS 10
#define TFT_DC 9
#define TFT_RST 8
#define TFT_BL 2
// I2C Sensors
#define SENSOR_SDA 6
#define SENSOR_SCL 7
#define MAX30102_ADDR 0x57
#define QMI8658_ADDR 0x6B
#define GY302_ADDR 0x23
// Buttons
#define BUTTON_POWER 0
#define BUTTON_MENU 1
// Screen dimensions
#define SCREEN_WIDTH 240
#define SCREEN_HEIGHT 320
// States
enum AppState {
STATE_WATCH_FACE,
STATE_NOTIFICATIONS,
STATE_QUICK_SETTINGS,
STATE_WEATHER_MUSIC,
STATE_LIVE_RECORDS,
STATE_MENU,
STATE_SETTINGS,
STATE_GAMES,
STATE_SOS,
STATE_FLASHLIGHT,
STATE_TIMER,
STATE_ALARM,
STATE_STOPWATCH,
STATE_BREATHING,
STATE_FIND_PHONE
};
// Watch Faces
enum WatchFaceType {
FACE_ANALOG_CLASSIC,
FACE_ANALOG_MODERN,
FACE_DIGITAL,
FACE_MINIMAL,
FACE_SPORT,
FACE_CHRONO,
FACE_NAVY,
FACE_GOLD,
FACE_RETRO,
FACE_CYBER,
FACE_NATURE,
FACE_ABSTRACT
};
// Themes
enum ThemeType {
THEME_MIDNIGHT, // Black
THEME_PEARL, // White
THEME_SAND, // Beige
THEME_GRAPHITE, // Gray
THEME_FOREST, // Green
THEME_OCEAN, // Blue
THEME_SUNSET, // Orange/Pink
THEME_ROYAL // Purple
};
// Game Types
enum GameType {
GAME_SNAKE,
GAME_BLOCK_PUZZLE,
GAME_RUNNER,
GAME_TIC_TAC_TOE,
GAME_INFINITY_LOOP,
GAME_MAZE
};
// Health Data Structure
struct HealthData {
uint8_t heartRate;
uint8_t spO2;
uint8_t stress;
uint16_t steps;
float distance;
uint16_t calories;
uint16_t activeMinutes;
uint8_t sleepQuality;
uint8_t sleepStage; // 0=deep, 1=light, 2=REM, 3=awake
float breathingRate;
};
// Sleep Data
struct SleepData {
uint32_t startTime;
uint32_t endTime;
uint8_t deepSleepMinutes;
uint8_t lightSleepMinutes;
uint8_t remMinutes;
uint8_t awakeMinutes;
uint8_t quality; // 0-100
};
// Notification
struct Notification {
char app[32];
char title[64];
char message[256];
uint32_t timestamp;
bool read;
};
// Settings
struct Settings {
WatchFaceType watchFace;
ThemeType theme;
uint8_t brightness;
bool vibrationEnabled;
bool doNotDisturb;
bool sleepMode;
bool pinLock;
char pin[5];
bool notificationEnabled;
uint8_t volume;
};
// Global Variables
AppState currentState = STATE_WATCH_FACE;
WatchFaceType currentFace = FACE_ANALOG_CLASSIC;
ThemeType currentTheme = THEME_MIDNIGHT;
Settings settings;
HealthData health;
SleepData sleepData;
Notification notifications[20];
uint8_t notificationCount = 0;
bool bluetoothConnected = false;
bool findPhoneActive = false;
uint32_t lastActivityTime = 0;
bool aodActive = false;
// Touch handling
int16_t touchX = 0, touchY = 0;
bool touchPressed = false;
uint32_t touchStartTime = 0;
bool longPressTriggered = false;
// Game variables
GameType currentGame = GAME_SNAKE;
bool gameActive = false;
int gameScore = 0;
// Time
ESP32Time rtc(0);
uint8_t currentHour = 12, currentMinute = 0, currentSecond = 0;
uint8_t currentDay = 1, currentMonth = 1, currentYear = 2024;
// Colors based on theme
uint16_t themeColors[8][5] = {
// Background, Primary, Secondary, Accent, Text
{TFT_BLACK, TFT_WHITE, 0x8410, 0xF800, TFT_WHITE}, // MIDNIGHT
{TFT_WHITE, TFT_BLACK, 0x8410, 0xF800, TFT_BLACK}, // PEARL
{0xEF5C, 0x2104, 0x8410, 0xF800, 0x2104}, // SAND
{0x4208, 0xC618, 0x8410, 0xFFE0, 0xC618}, // GRAPHITE
{0x0204, 0x07E0, 0x8410, 0xFFE0, 0x07E0}, // FOREST
{0x0005, 0x001F, 0x8410, 0x07FF, 0x001F}, // OCEAN
{0x4008, 0xFBE0, 0x8410, 0xF800, 0xFBE0}, // SUNSET
{0x2008, 0x801F, 0x8410, 0xF81F, 0x801F} // ROYAL
};
// Flashlight state
bool flashlightActive = false;
// Function Prototypes
void initHardware();
void initSensors();
void initBluetooth();
void initDisplay();
void initTouch();
void updateSensors();
void readTouch();
void handleGestures();
void drawWatchFace();
void drawAnalogFace(uint16_t bg, uint16_t primary, uint16_t accent);
void drawDigitalFace(uint16_t bg, uint16_t primary, uint16_t accent);
void drawMinimalFace(uint16_t bg, uint16_t primary, uint16_t accent);
void drawActivityCorners();
void drawNotifications();
void drawQuickSettings();
void drawWeatherMusic();
void drawLiveRecords();
void drawMenu();
void drawSettings();
void drawGames();
void drawSOS();
void drawFlashlight();
void drawTimer();
void drawAlarm();
void drawStopwatch();
void drawBreathing();
void drawFindPhone();
void handleButtonPower();
void handleButtonMenu();
void updateHealthSensors();
void updateQMI8658();
void updateMAX30102();
void updateGY302();
void sendSOS();
void playGame();
void updateSleep();
uint8_t calculateSleepQuality();
void saveSettings();
void loadSettings();
void setBrightness(uint8_t level);
void vibrate(uint16_t duration);
void showAOD();
void connectToPhone();
void sendFindPhone();
void receiveNotification(const char* app, const char* title, const char* message);
void drawThemeBackground();
uint16_t getColor(uint8_t colorIndex);
float calculateDistance(uint16_t steps);
uint16_t calculateCalories(uint16_t steps, uint8_t heartRate);
void breathingExercise();
void updateWeather();
void updateMusicControl();
void drawGameSnake();
void drawGameBlockPuzzle();
void drawGameRunner();
void drawGameTicTacToe();
void drawGameInfinityLoop();
void drawGameMaze();
void handleGameInput();
void handleBluetoothData();
int8_t getBatteryLevel();
void showAbout();
void showPowerMenu();
bool checkBlockCollision();
bool isLoopConnected(int x, int y);
bool isPuzzleSolved();
// Array of accent colors for notifications
uint16_t accentColors[] = {
TFT_RED, TFT_GREEN, TFT_BLUE, TFT_YELLOW, TFT_CYAN, TFT_MAGENTA
};
// Game globals (Moved to top to prevent errors)
int8_t dirX = 1, dirY = 0;
float playerVelY = 0;
uint8_t maze[15][15];
int8_t playerX = 1, playerY = 1;
// ==================== INITIALIZATION ====================
void setup() {
Serial.begin(115200);
delay(1000);
Serial.println("ESP32-S3 Smartwatch Starting...");
initHardware();
initSensors();
initBluetooth();
initDisplay();
initTouch();
loadSettings();
Serial.println("Smartwatch initialized successfully!");
Serial.println("Swipe: UP=Quick Settings, DOWN=Notifications, LEFT=Live Records, RIGHT=Weather/Music");
Serial.println("Long press watch face to change faces");
Serial.println("Power button = Home, Menu button = App Menu");
}
void initHardware() {
pinMode(BUTTON_POWER, INPUT_PULLUP);
pinMode(BUTTON_MENU, INPUT_PULLUP);
pinMode(TFT_BL, OUTPUT);
Wire.begin(SENSOR_SDA, SENSOR_SCL);
// Initialize QMI8658 (Keep this to ensure I2C bus is active for touch)
Wire.beginTransmission(QMI8658_ADDR);
Wire.write(0x60); // CTRL1
Wire.write(0x00); // Enable accelerometer and gyroscope
Wire.endTransmission();
Serial.println("Hardware initialized");
}
// ---------------------------------------------------------
// [Modification 1]: Bypass hardware sensor initialization and load dummy data for testing.
// ---------------------------------------------------------
void initSensors() {
health.heartRate = 72;
health.spO2 = 98;
health.steps = 1420;
health.stress = 35;
health.calories = 420;
health.activeMinutes = 45;
health.sleepQuality = 85;
Serial.println("The analog sensor has been initialized.");
}
void initBluetooth() {
#ifdef ENABLE_BLE
ble.begin("SmartWatch-Pro");
Serial.println("BLE ready. Connect to 'SmartWatch-Pro'");
#else
Serial.println("BLE disabled - skipping initialization");
#endif
}
void initDisplay() {
tft.init();
tft.setRotation(0);
tft.fillScreen(TFT_BLACK);
setBrightness(settings.brightness);
Serial.println("Display initialized");
}
void initTouch() {
pinMode(TOUCH_INT, INPUT);
pinMode(TOUCH_RST, OUTPUT);
digitalWrite(TOUCH_RST, HIGH);
delay(10);
digitalWrite(TOUCH_RST, LOW);
delay(10);
digitalWrite(TOUCH_RST, HIGH);
delay(10);
Serial.println("Touch initialized");
}
// ==================== MAIN LOOP ====================
void loop() {
static uint32_t lastSensorUpdate = 0;
static uint32_t lastDisplayUpdate = 0;
static uint32_t lastSecond = 0;
uint32_t currentMillis = millis();
// Update time
if (currentMillis - lastSecond >= 1000) {
lastSecond = currentMillis;
currentSecond++;
if (currentSecond >= 60) {
currentSecond = 0;
currentMinute++;
if (currentMinute >= 60) {
currentMinute = 0;
currentHour++;
if (currentHour >= 24) {
currentHour = 0;
currentDay++;
}
}
}
}
// Update sensors every 100ms
if (currentMillis - lastSensorUpdate >= 100) {
lastSensorUpdate = currentMillis;
updateSensors();
}
// Handle Bluetooth
if (ble.available()) {
handleBluetoothData();
}
// Read touch input
readTouch();
handleGestures();
// Handle buttons
if (digitalRead(BUTTON_POWER) == LOW) {
delay(50);
if (digitalRead(BUTTON_POWER) == LOW) {
handleButtonPower();
while (digitalRead(BUTTON_POWER) == LOW);
}
}
if (digitalRead(BUTTON_MENU) == LOW) {
delay(50);
if (digitalRead(BUTTON_MENU) == LOW) {
handleButtonMenu();
while (digitalRead(BUTTON_MENU) == LOW);
}
}
// Update display every 50ms (20fps)
if (currentMillis - lastDisplayUpdate >= 50) {
lastDisplayUpdate = currentMillis;
updateDisplay();
}
// AOD timeout (5 seconds of inactivity)
if (currentMillis - lastActivityTime > 5000 && !aodActive) {
showAOD();
}
delay(10);
}
// ==================== SENSOR HANDLING ====================
void updateSensors() {
updateQMI8658();
updateMAX30102();
updateGY302();
// Calculate derived metrics
health.distance = calculateDistance(health.steps);
health.calories = calculateCalories(health.steps, health.heartRate);
}
// ---------------------------------------------------------
// [Modification 2]: Pedometer simulation - Add one step every two seconds
// ---------------------------------------------------------
void updateQMI8658() {
static uint32_t lastStepTime = 0;
if (millis() - lastStepTime > 2000) {
health.steps++;
lastStepTime = millis();
if (health.steps % 100 == 0) {
health.activeMinutes++;
}
}
}
// ---------------------------------------------------------
// [Modification Point 3]: Heart Rate and Blood Oxygen Simulation - Random beats every three seconds
// ---------------------------------------------------------
void updateMAX30102() {
static uint32_t lastHRUpdate = 0;
if (millis() - lastHRUpdate > 3000) {
health.heartRate = random(68, 85);
health.spO2 = random(96, 100);
health.stress = random(25, 45);
lastHRUpdate = millis();
}
}
// ---------------------------------------------------------
// [Modification Point 4]: Light sensor simulation - lock screen brightness to prevent black screen.
// ---------------------------------------------------------
void updateGY302() {
if (!settings.pinLock) {
if (settings.brightness != 150) {
settings.brightness = 150; // 將亮度固定在 150
setBrightness(settings.brightness);
}
}
}
void updateSleep() {
static uint32_t sleepStartTime = 0;
static bool wasSleeping = false;
static uint32_t lastMovement = 0;
uint32_t currentMillis = millis();
// Detect sleep start
if (!wasSleeping && sleepStartTime == 0) {
sleepStartTime = currentMillis;
wasSleeping = true;
}
// Monitor movement for sleep stages
if (currentMillis - lastMovement > 300000) { // 5 minutes no movement
sleepData.deepSleepMinutes++;
} else if (currentMillis - lastMovement > 60000) { // 1 minute no movement
sleepData.lightSleepMinutes++;
} else {
sleepData.remMinutes++;
}
lastMovement = currentMillis;
// Calculate sleep quality
sleepData.quality = calculateSleepQuality();
health.sleepQuality = sleepData.quality;
}
uint8_t calculateSleepQuality() {
uint32_t totalSleepMinutes = sleepData.deepSleepMinutes + sleepData.lightSleepMinutes +
sleepData.remMinutes + sleepData.awakeMinutes;
if (totalSleepMinutes == 0) return 0;
// Deep sleep should be 15-20% of total sleep
float deepSleepPercent = (sleepData.deepSleepMinutes * 100.0) / totalSleepMinutes;
uint8_t deepSleepScore = map(deepSleepPercent, 0, 25, 0, 40);
// REM should be 20-25%
float remPercent = (sleepData.remMinutes * 100.0) / totalSleepMinutes;
uint8_t remScore = map(remPercent, 0, 30, 0, 30);
// Minimize awake time
float awakePercent = (sleepData.awakeMinutes * 100.0) / totalSleepMinutes;
uint8_t continuityScore = map(awakePercent, 50, 0, 0, 30);
uint8_t quality = deepSleepScore + remScore + continuityScore;
if (quality > 100) quality = 100;
return quality;
}
// ==================== TOUCH & GESTURES ====================
void readTouch() {
Wire.beginTransmission(0x38);
Wire.write(0x02);
Wire.endTransmission(false);
Wire.requestFrom(0x38, 5);
if (Wire.available() >= 5) {
uint8_t touches = Wire.read();
uint8_t x_high = Wire.read();
uint8_t x_low = Wire.read();
uint8_t y_high = Wire.read();
uint8_t y_low = Wire.read();
if (touches > 0 && touches < 3) {
uint16_t x = ((x_high & 0x0F) << 8) | x_low;
uint16_t y = ((y_high & 0x0F) << 8) | y_low;
touchX = x;
touchY = y;
touchPressed = true;
if (touchStartTime == 0) {
touchStartTime = millis();
}
lastActivityTime = millis();
if (aodActive) {
aodActive = false;
setBrightness(settings.brightness);
}
return;
}
}
touchPressed = false;
touchStartTime = 0;
longPressTriggered = false;
}
void handleGestures() {
static int16_t startX = 0, startY = 0;
static bool gestureStarted = false;
if (touchPressed && !gestureStarted) {
startX = touchX;
startY = touchY;
gestureStarted = true;
}
if (!touchPressed && gestureStarted) {
int16_t deltaX = touchX - startX;
int16_t deltaY = touchY - startY;
// Determine swipe direction
if (abs(deltaX) > abs(deltaY)) {
if (abs(deltaX) > 30) { // Minimum swipe distance
if (deltaX > 0) {
// Swipe Right - Weather/Music
if (currentState == STATE_WATCH_FACE) {
currentState = STATE_WEATHER_MUSIC;
vibrate(50);
}
} else {
// Swipe Left - Live Records
if (currentState == STATE_WATCH_FACE) {
currentState = STATE_LIVE_RECORDS;
vibrate(50);
}
}
}
} else {
if (abs(deltaY) > 30) {
if (deltaY > 0) {
// Swipe Down - Notifications
if (currentState == STATE_WATCH_FACE) {
currentState = STATE_NOTIFICATIONS;
vibrate(50);
}
} else {
// Swipe Up - Quick Settings
if (currentState == STATE_WATCH_FACE) {
currentState = STATE_QUICK_SETTINGS;
vibrate(50);
}
}
}
}
gestureStarted = false;
}
// Long press detection for watch face change
if (touchPressed && currentState == STATE_WATCH_FACE) {
if (millis() - touchStartTime > 800 && !longPressTriggered) {
longPressTriggered = true;
// Cycle through watch faces
currentFace = (WatchFaceType)((currentFace + 1) % 12);
settings.watchFace = currentFace;
saveSettings();
vibrate(100);
drawWatchFace();
}
}
}
void handleButtonPower() {
if (currentState != STATE_WATCH_FACE) {
currentState = STATE_WATCH_FACE;
} else {
// Toggle screen on/off
static bool screenOn = true;
screenOn = !screenOn;
setBrightness(screenOn ? settings.brightness : 0);
}
vibrate(30);
}
void handleButtonMenu() {
if (currentState == STATE_MENU) {
currentState = STATE_WATCH_FACE;
} else {
currentState = STATE_MENU;
}
vibrate(30);
}
// ==================== DISPLAY FUNCTIONS ====================
void updateDisplay() {
switch (currentState) {
case STATE_WATCH_FACE:
drawWatchFace();
break;
case STATE_NOTIFICATIONS:
drawNotifications();
break;
case STATE_QUICK_SETTINGS:
drawQuickSettings();
break;
case STATE_WEATHER_MUSIC:
drawWeatherMusic();
break;
case STATE_LIVE_RECORDS:
drawLiveRecords();
break;
case STATE_MENU:
drawMenu();
break;
case STATE_SETTINGS:
drawSettings();
break;
case STATE_GAMES:
drawGames();
break;
case STATE_SOS:
drawSOS();
break;
case STATE_FLASHLIGHT:
drawFlashlight();
break;
case STATE_TIMER:
drawTimer();
break;
case STATE_ALARM:
drawAlarm();
break;
case STATE_STOPWATCH:
drawStopwatch();
break;
case STATE_BREATHING:
drawBreathing();
break;
case STATE_FIND_PHONE:
drawFindPhone();
break;
}
}
void drawThemeBackground() {
uint16_t bgColor = getColor(0);
tft.fillScreen(bgColor);
}
uint16_t getColor(uint8_t colorIndex) {
return themeColors[currentTheme][colorIndex];
}
void drawWatchFace() {
drawThemeBackground();
uint16_t bg = getColor(0);
uint16_t primary = getColor(1);
uint16_t secondary = getColor(2);
uint16_t accent = getColor(3);
switch (currentFace) {
case FACE_ANALOG_CLASSIC:
case FACE_ANALOG_MODERN:
case FACE_CHRONO:
case FACE_NAVY:
case FACE_GOLD:
case FACE_RETRO:
drawAnalogFace(bg, primary, accent);
break;
case FACE_DIGITAL:
case FACE_CYBER:
drawDigitalFace(bg, primary, accent);
break;
case FACE_MINIMAL:
case FACE_SPORT:
case FACE_NATURE:
case FACE_ABSTRACT:
drawMinimalFace(bg, primary, accent);
break;
}
drawActivityCorners();
}
void drawAnalogFace(uint16_t bg, uint16_t primary, uint16_t accent) {
int16_t centerX = SCREEN_WIDTH / 2;
int16_t centerY = SCREEN_HEIGHT / 2 - 20;
int16_t radius = min(SCREEN_WIDTH, SCREEN_HEIGHT) / 2 - 25;
// Draw watch case/ring
tft.drawCircle(centerX, centerY, radius + 3, primary);
tft.drawCircle(centerX, centerY, radius + 2, getColor(2)); // secondary
// Draw hour markers
for (int i = 0; i < 12; i++) {
float angle = (i * 30 - 90) * PI / 180;
int16_t x1 = centerX + (radius - 8) * cos(angle);
int16_t y1 = centerY + (radius - 8) * sin(angle);
int16_t x2 = centerX + (radius - 2) * cos(angle);
int16_t y2 = centerY + (radius - 2) * sin(angle);
uint16_t color = (i % 3 == 0) ? accent : primary;
tft.drawLine(x1, y1, x2, y2, color);
if (i % 3 == 0) {
// Draw numbers at 12, 3, 6, 9
int16_t numX = centerX + (radius - 20) * cos(angle);
int16_t numY = centerY + (radius - 20) * sin(angle);
tft.setTextColor(primary, bg);
tft.setTextDatum(MC_DATUM);
tft.setTextSize(1);
tft.drawNumber(i == 0 ? 12 : i, numX, numY);
}
}
// Calculate hand angles
float hourAngle = ((currentHour % 12) * 30 + currentMinute * 0.5 - 90) * PI / 180;
float minuteAngle = (currentMinute * 6 + currentSecond * 0.1 - 90) * PI / 180;
float secondAngle = (currentSecond * 6 - 90) * PI / 180;
// Draw hour hand
int16_t hourX = centerX + (radius * 0.5) * cos(hourAngle);
int16_t hourY = centerY + (radius * 0.5) * sin(hourAngle);
tft.drawLine(centerX, centerY, hourX, hourY, primary);
tft.fillCircle(hourX, hourY, 2, primary);
// Draw minute hand
int16_t minX = centerX + (radius * 0.75) * cos(minuteAngle);
int16_t minY = centerY + (radius * 0.75) * sin(minuteAngle);
tft.drawLine(centerX, centerY, minX, minY, primary);
tft.fillCircle(minX, minY, 2, primary);
// Draw second hand
int16_t secX = centerX + (radius * 0.85) * cos(secondAngle);
int16_t secY = centerY + (radius * 0.85) * sin(secondAngle);
tft.drawLine(centerX, centerY, secX, secY, accent);
tft.fillCircle(secX, secY, 2, accent);
// Center dot
tft.fillCircle(centerX, centerY, 4, accent);
tft.fillCircle(centerX, centerY, 2, bg);
// Date
tft.setTextColor(primary, bg);
tft.setTextDatum(BC_DATUM);
tft.setTextSize(1);
char dateStr[20];
sprintf(dateStr, "%02d/%02d %s", currentDay, currentMonth,
currentHour >= 12 ? "PM" : "AM");
tft.drawString(dateStr, centerX, centerY + radius + 15);
}
void drawDigitalFace(uint16_t bg, uint16_t primary, uint16_t accent) {
char timeStr[10];
sprintf(timeStr, "%02d:%02d", currentHour, currentMinute);
tft.setTextColor(primary, bg);
tft.setTextDatum(MC_DATUM);
tft.setTextSize(3);
tft.drawString(timeStr, SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2 - 30);
// Seconds
tft.setTextSize(2);
tft.setTextColor(accent, bg);
sprintf(timeStr, ":%02d", currentSecond);
tft.drawString(timeStr, SCREEN_WIDTH / 2 + 80, SCREEN_HEIGHT / 2 - 30);
// Date
tft.setTextSize(1);
tft.setTextColor(primary, bg);
sprintf(timeStr, "%02d/%02d/%04d", currentDay, currentMonth, currentYear);
tft.drawString(timeStr, SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2 + 20);
// Day of week
const char* days[] = {"SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"};
tft.drawString(days[0], SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2 + 40);
}
void drawMinimalFace(uint16_t bg, uint16_t primary, uint16_t accent) {
int16_t centerX = SCREEN_WIDTH / 2;
int16_t centerY = SCREEN_HEIGHT / 2 - 20;
// Simple ring
tft.drawCircle(centerX, centerY, 80, primary);
// Time as numbers
char timeStr[10];
sprintf(timeStr, "%02d:%02d", currentHour, currentMinute);
tft.setTextColor(primary, bg);
tft.setTextDatum(MC_DATUM);
tft.setTextSize(2);
tft.drawString(timeStr, centerX, centerY - 10);
// Small seconds
tft.setTextSize(1);
tft.setTextColor(accent, bg);
sprintf(timeStr, "%02d", currentSecond);
tft.drawString(timeStr, centerX, centerY + 15);
}
void drawActivityCorners() {
uint16_t bg = getColor(0);
uint16_t primary = getColor(1);
uint16_t accent = getColor(3);
// Top left - Steps
tft.setTextSize(1);
tft.setTextDatum(TL_DATUM);
tft.setTextColor(primary, bg);
tft.drawString("STEPS", 5, 5);
tft.setTextColor(accent, bg);
tft.drawNumber(health.steps, 5, 18);
// Top right - Heart Rate
tft.setTextDatum(TR_DATUM);
tft.setTextColor(primary, bg);
tft.drawString("BPM", SCREEN_WIDTH - 5, 5);
tft.setTextColor(accent, bg);
char hrStr[5];
sprintf(hrStr, "%d", health.heartRate);
tft.drawString(hrStr, SCREEN_WIDTH - 5, 18);
// Bottom left - Battery
int8_t batteryLevel = getBatteryLevel();
tft.setTextDatum(BL_DATUM);
tft.setTextColor(primary, bg);
tft.drawString("BAT", 5, SCREEN_HEIGHT - 25);
tft.setTextColor(batteryLevel < 20 ? TFT_RED : accent, bg);
char batStr[5];
sprintf(batStr, "%d%%", batteryLevel);
tft.drawString(batStr, 5, SCREEN_HEIGHT - 12);
// Bottom right - Bluetooth
tft.setTextDatum(BR_DATUM);
tft.setTextColor(bluetoothConnected ? accent : primary, bg);
tft.drawString(bluetoothConnected ? "BT" : "--", SCREEN_WIDTH - 5, SCREEN_HEIGHT - 18);
}
void drawNotifications() {
drawThemeBackground();
uint16_t primary = getColor(1);
uint16_t secondary = getColor(2);
// Header
tft.setTextColor(primary, getColor(0));
tft.setTextDatum(TC_DATUM);
tft.setTextSize(2);
tft.drawString("Notifications", SCREEN_WIDTH / 2, 10);
// Notification list
tft.setTextSize(1);
tft.setTextDatum(TL_DATUM);
int y = 50;
for (int i = 0; i < min((int)notificationCount, 5); i++) {
// Notification card background
tft.fillRoundRect(10, y, SCREEN_WIDTH - 20, 45, 5, secondary);
// App name
tft.setTextColor(accentColors[notifications[i].app[0] % 6], secondary);
tft.drawString(notifications[i].app, 15, y + 5);
// Title
tft.setTextColor(primary, secondary);
tft.drawString(notifications[i].title, 15, y + 20);
// Time
char timeStr[10];
sprintf(timeStr, "%02d:%02d",
(notifications[i].timestamp / 3600) % 24,
(notifications[i].timestamp / 60) % 60);
tft.setTextDatum(TR_DATUM);
tft.drawString(timeStr, SCREEN_WIDTH - 15, y + 5);
tft.setTextDatum(TL_DATUM);
y += 50;
}
if (notificationCount == 0) {
tft.setTextDatum(MC_DATUM);
tft.setTextSize(1);
tft.setTextColor(secondary, getColor(0));
tft.drawString("No new notifications", SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2);
}
// Swipe up hint
tft.setTextDatum(BC_DATUM);
tft.setTextColor(secondary, getColor(0));
tft.drawString("Swipe up to close", SCREEN_WIDTH / 2, SCREEN_HEIGHT - 10);
}
void drawQuickSettings() {
drawThemeBackground();
uint16_t bg = getColor(0);
uint16_t primary = getColor(1);
uint16_t secondary = getColor(2);
uint16_t accent = getColor(3);
// Header
tft.setTextColor(primary, bg);
tft.setTextDatum(TC_DATUM);
tft.setTextSize(2);
tft.drawString("Quick Settings", SCREEN_WIDTH / 2, 10);
// Date
char dateStr[30];
sprintf(dateStr, "%02d/%02d/%04d", currentDay, currentMonth, currentYear);
tft.setTextSize(1);
tft.setTextColor(secondary, bg);
tft.drawString(dateStr, SCREEN_WIDTH / 2, 35);
// Grid layout for buttons
const char* icons[] = {"Flash", "Find", "Alarm", "Set"};
const char* labels[] = {"Light", "Phone", "Clock", "tings"};
int startY = 55;
int buttonW = 50;
int buttonH = 50;
int gap = 15;
int startX = (SCREEN_WIDTH - (2 * buttonW + gap)) / 2;
for (int i = 0; i < 4; i++) {
int x = startX + (i % 2) * (buttonW + gap);
int y = startY + (i / 2) * (buttonH + gap + 15);
// Button background
uint16_t btnColor = (i == 0 && flashlightActive) ? accent : secondary;
tft.fillRoundRect(x, y, buttonW, buttonH, 8, btnColor);
// Icon (simplified as text for now)
tft.setTextColor(primary, btnColor);
tft.setTextDatum(MC_DATUM);
tft.setTextSize(1);
tft.drawString(icons[i], x + buttonW / 2, y + buttonH / 2 - 5);
tft.drawString(labels[i], x + buttonW / 2, y + buttonH / 2 + 8);
}
// Battery level bar
int8_t batteryLevel = getBatteryLevel();
int barWidth = SCREEN_WIDTH - 40;
int barHeight = 10;
int barX = 20;
int barY = 195;
tft.drawRoundRect(barX, barY, barWidth, barHeight, 3, primary);
tft.fillRoundRect(barX + 2, barY + 2,
(barWidth - 4) * batteryLevel / 100, barHeight - 4, 2,
batteryLevel > 20 ? accent : TFT_RED);
// Battery percentage
tft.setTextColor(primary, bg);
tft.setTextDatum(TC_DATUM);
char batStr[10];
sprintf(batStr, "%d%%", batteryLevel);
tft.drawString(batStr, SCREEN_WIDTH / 2, barY + 15);
// Brightness slider
tft.setTextDatum(TL_DATUM);
tft.drawString("Brightness", 20, 225);
tft.drawRoundRect(20, 240, SCREEN_WIDTH - 40, 10, 3, primary);
tft.fillRoundRect(22, 242, (SCREEN_WIDTH - 44) * settings.brightness / 255, 6, 2, accent);
// Connection status
tft.setTextDatum(TR_DATUM);
tft.setTextColor(bluetoothConnected ? accent : TFT_RED, bg);
tft.drawString(bluetoothConnected ? "Connected" : "Disconnected", SCREEN_WIDTH - 20, 225);
// Touch handlers for quick settings
if (touchPressed) {
if (touchX >= startX && touchX <= startX + buttonW &&
touchY >= startY && touchY <= startY + buttonH) {
currentState = STATE_FLASHLIGHT;
vibrate(30);
} else if (touchX >= startX + buttonW + gap && touchX <= startX + 2 * buttonW + gap &&
touchY >= startY && touchY <= startY + buttonH) {
sendFindPhone();
vibrate(30);
} else if (touchX >= startX && touchX <= startX + buttonW &&
touchY >= startY + buttonH + gap + 15 && touchY <= startY + 2 * buttonH + gap + 15) {
currentState = STATE_ALARM;
vibrate(30);
} else if (touchX >= startX + buttonW + gap && touchX <= startX + 2 * buttonW + gap &&
touchY >= startY + buttonH + gap + 15 && touchY <= startY + 2 * buttonH + gap + 15) {
currentState = STATE_SETTINGS;
vibrate(30);
}
}
}
void drawWeatherMusic() {
drawThemeBackground();
uint16_t bg = getColor(0);
uint16_t primary = getColor(1);
uint16_t secondary = getColor(2);
uint16_t accent = getColor(3);
// Header
tft.setTextColor(primary, bg);
tft.setTextDatum(TC_DATUM);
tft.setTextSize(2);
tft.drawString("Weather & Music", SCREEN_WIDTH / 2, 10);
// Weather section
int weatherY = 50;
tft.fillRoundRect(15, weatherY, SCREEN_WIDTH - 30, 100, 10, secondary);
// Temperature (large)
tft.setTextColor(accent, secondary);
tft.setTextDatum(MC_DATUM);
tft.setTextSize(3);
tft.drawString("72°F", SCREEN_WIDTH / 2, weatherY + 35);
// Condition
tft.setTextColor(primary, secondary);
tft.setTextSize(1);
tft.drawString("Partly Cloudy", SCREEN_WIDTH / 2, weatherY + 65);
// Details
char details[50];
sprintf(details, "H:75° L:68° Hum:65%%");
tft.drawString(details, SCREEN_WIDTH / 2, weatherY + 80);
// Music control section
int musicY = 160;
tft.fillRoundRect(15, musicY, SCREEN_WIDTH - 30, 100, 10, secondary);
// Song info
tft.setTextColor(primary, secondary);
tft.setTextSize(1);
tft.drawString("Now Playing", SCREEN_WIDTH / 2, musicY + 15);
tft.setTextColor(accent, secondary);
tft.setTextSize(1);
tft.drawString("Song Title - Artist", SCREEN_WIDTH / 2, musicY + 35);
// Progress bar
tft.drawRoundRect(30, musicY + 50, SCREEN_WIDTH - 60, 5, 2, primary);
tft.fillRoundRect(30, musicY + 50, (SCREEN_WIDTH - 60) / 3, 5, 2, accent);
// Control buttons
int btnY = musicY + 70;
int btnSize = 25;
int gap = 30;
int startX = (SCREEN_WIDTH - (3 * btnSize + 2 * gap)) / 2;
// Previous
tft.fillTriangle(startX + 5, btnY, startX + 5, btnY + btnSize,
startX + btnSize, btnY + btnSize/2, primary);
// Play/Pause
tft.fillCircle(startX + btnSize + gap + btnSize/2, btnY + btnSize/2, btnSize/2, accent);
tft.fillRect(startX + btnSize + gap + 7, btnY + 6, 4, 13, bg);
tft.fillRect(startX + btnSize + gap + 14, btnY + 6, 4, 13, bg);
// Next
tft.fillTriangle(startX + 2*btnSize + 2*gap + btnSize, btnY,
startX + 2*btnSize + 2*gap + btnSize, btnY + btnSize,
startX + 2*btnSize + 2*gap, btnY + btnSize/2, primary);
}
void drawLiveRecords() {
drawThemeBackground();
uint16_t bg = getColor(0);
uint16_t primary = getColor(1);
uint16_t secondary = getColor(2);
uint16_t accent = getColor(3);
// Header
tft.setTextColor(primary, bg);
tft.setTextDatum(TC_DATUM);
tft.setTextSize(2);
tft.drawString("Live Health", SCREEN_WIDTH / 2, 10);
// 2x4 grid of circular health metrics
const char* labels[] = {"Heart", "SpO2", "Sleep", "Stress", "Cal", "Active", "Steps", "Breathe"};
uint8_t values[] = {health.heartRate, health.spO2, health.sleepQuality, health.stress,
health.calories / 10, health.activeMinutes,
health.steps > 255 ? 255 : health.steps, 0};
const char* units[] = {"bpm", "%", "%", "%", "cal", "min", "", ""};
int startY = 50;
int circleR = 35;
int gapX = 25;
int gapY = 25;
int startX = (SCREEN_WIDTH - (2 * circleR * 2 + gapX)) / 2 + circleR;
for (int i = 0; i < 8; i++) {
int col = i % 2;
int row = i / 2;
int x = startX + col * (circleR * 2 + gapX);
int y = startY + row * (circleR * 2 + gapY) + circleR;
// Circle background
tft.fillCircle(x, y, circleR, secondary);
// Progress ring
float progress = (i < 7) ? values[i] / 100.0 : 0;
for (int a = 0; a < 360 * progress; a += 5) {
float rad = (a - 90) * PI / 180;
int px = x + (circleR - 5) * cos(rad);
int py = y + (circleR - 5) * sin(rad);
tft.drawPixel(px, py, accent);
}
// Value
tft.setTextColor(accent, secondary);
tft.setTextDatum(MC_DATUM);
tft.setTextSize(1);
if (i == 6) { // Steps (need smaller text or just icon)
tft.drawString("...", x, y - 5);
} else if (i == 7) { // Breathing exercise
tft.drawString("Breathe", x, y);
} else {
char valStr[10];
if (i == 4) {
sprintf(valStr, "%d", values[i] * 10);
} else {
sprintf(valStr, "%d", values[i]);
}
tft.drawString(valStr, x, y - 5);
}
// Label
tft.setTextColor(primary, secondary);
tft.setTextSize(1);
tft.drawString(labels[i], x, y + 10);
// Unit (small)
if (i < 6) {
tft.setTextColor(secondary, secondary);
tft.drawString(units[i], x, y + 2);
}
}
// Touch handler for breathing exercise (bottom right circle)
if (touchPressed) {
int breatheX = startX + circleR * 2 + gapX - circleR;
int breatheY = startY + 3 * (circleR * 2 + gapY) + circleR;
int dist = sqrt((touchX - breatheX) * (touchX - breatheX) +
(touchY - breatheY) * (touchY - breatheY));
if (dist < circleR) {
currentState = STATE_BREATHING;
vibrate(30);
}
}
}
void drawMenu() {
drawThemeBackground();
uint16_t bg = getColor(0);
uint16_t primary = getColor(1);
uint16_t secondary = getColor(2);
uint16_t accent = getColor(3);
// Header
tft.setTextColor(primary, bg);
tft.setTextDatum(TC_DATUM);
tft.setTextSize(2);
tft.drawString("Menu", SCREEN_WIDTH / 2, 10);
// App grid
const char* apps[] = {
"Timer", "Alarm", "Stopwatch", "Flashlight",
"Games", "SOS", "Find Phone", "Settings",
"Breathing", "Weather", "Music", "About"
};
int startY = 50;
int appSize = 50;
int gap = 15;
int cols = 3;
int startX = (SCREEN_WIDTH - (cols * appSize + (cols - 1) * gap)) / 2;
for (int i = 0; i < 12; i++) {
int col = i % cols;
int row = i / cols;
int x = startX + col * (appSize + gap);
int y = startY + row * (appSize + gap + 15);
// App icon background
tft.fillRoundRect(x, y, appSize, appSize, 8, secondary);
// App name
tft.setTextColor(primary, secondary);
tft.setTextDatum(MC_DATUM);
tft.setTextSize(1);
tft.drawString(apps[i], x + appSize / 2, y + appSize / 2);
}
// Touch handling
if (touchPressed) {
int col = (touchX - startX) / (appSize + gap);
int row = (touchY - startY) / (appSize + gap + 15);
if (col >= 0 && col < cols && row >= 0 && row < 4) {
int appIndex = row * cols + col;
switch (appIndex) {
case 0: currentState = STATE_TIMER; break;
case 1: currentState = STATE_ALARM; break;
case 2: currentState = STATE_STOPWATCH; break;
case 3: currentState = STATE_FLASHLIGHT; break;
case 4: currentState = STATE_GAMES; break;
case 5: currentState = STATE_SOS; break;
case 6: currentState = STATE_FIND_PHONE; break;
case 7: currentState = STATE_SETTINGS; break;
case 8: currentState = STATE_BREATHING; break;
case 9: currentState = STATE_WEATHER_MUSIC; break;
case 10: currentState = STATE_WEATHER_MUSIC; break;
case 11: showAbout(); break;
}
vibrate(30);
}
}
}
void drawSettings() {
drawThemeBackground();
uint16_t bg = getColor(0);
uint16_t primary = getColor(1);
uint16_t secondary = getColor(2);
uint16_t accent = getColor(3);
// Header
tft.setTextColor(primary, bg);
tft.setTextDatum(TC_DATUM);
tft.setTextSize(2);
tft.drawString("Settings", SCREEN_WIDTH / 2, 10);
// Settings list
const char* settingsItems[] = {
"Watch Faces", "Display", "Vibration", "Do Not Disturb",
"Sleep Mode", "PIN Lock", "Notifications", "Bluetooth",
"Power Settings", "About"
};
int itemHeight = 30;
int startY = 50;
for (int i = 0; i < 10; i++) {
int y = startY + i * itemHeight;
// Alternating background
if (i % 2 == 0) {
tft.fillRect(0, y, SCREEN_WIDTH, itemHeight, secondary);
}
// Setting name
tft.setTextColor(primary, i % 2 == 0 ? secondary : bg);
tft.setTextDatum(TL_DATUM);
tft.setTextSize(1);
tft.drawString(settingsItems[i], 15, y + 8);
// Toggle or arrow
tft.setTextDatum(TR_DATUM);
if (i == 3 && settings.doNotDisturb) {
tft.setTextColor(accent, i % 2 == 0 ? secondary : bg);
tft.drawString("ON", SCREEN_WIDTH - 15, y + 8);
} else if (i == 4 && settings.sleepMode) {
tft.setTextColor(accent, i % 2 == 0 ? secondary : bg);
tft.drawString("ON", SCREEN_WIDTH - 15, y + 8);
} else if (i == 5 && settings.pinLock) {
tft.setTextColor(accent, i % 2 == 0 ? secondary : bg);
tft.drawString("ON", SCREEN_WIDTH - 15, y + 8);
} else if (i == 7) {
tft.setTextColor(bluetoothConnected ? accent : primary, i % 2 == 0 ? secondary : bg);
tft.drawString(bluetoothConnected ? "Conn" : "Disc", SCREEN_WIDTH - 15, y + 8);
} else {
tft.setTextColor(secondary, i % 2 == 0 ? secondary : bg);
tft.drawString(">", SCREEN_WIDTH - 15, y + 8);
}
}
// Touch handling
if (touchPressed) {
int itemIndex = (touchY - startY) / itemHeight;
if (itemIndex >= 0 && itemIndex < 10) {
switch (itemIndex) {
case 0: // Watch Faces
currentFace = (WatchFaceType)((currentFace + 1) % 12);
settings.watchFace = currentFace;
saveSettings();
break;
case 1: // Display
currentTheme = (ThemeType)((currentTheme + 1) % 8);
break;
case 2: // Vibration
settings.vibrationEnabled = !settings.vibrationEnabled;
if (settings.vibrationEnabled) vibrate(100);
break;
case 3: // Do Not Disturb
settings.doNotDisturb = !settings.doNotDisturb;
break;
case 4: // Sleep Mode
settings.sleepMode = !settings.sleepMode;
break;
case 5: // PIN Lock
settings.pinLock = !settings.pinLock;
break;
case 6: // Notifications
settings.notificationEnabled = !settings.notificationEnabled;
break;
case 7: // Bluetooth
bluetoothConnected = !bluetoothConnected;
if (bluetoothConnected) {
Serial.println("BLE connection enabled");
} else {
Serial.println("BLE connection disabled");
}
break;
case 8: // Power Settings
showPowerMenu();
break;
case 9: // About
showAbout();
break;
}
saveSettings();
vibrate(30);
}
}
}
void drawSOS() {
drawThemeBackground();
uint16_t bg = getColor(0);
uint16_t primary = getColor(1);
uint16_t accent = TFT_RED;
// Header
tft.setTextColor(accent, bg);
tft.setTextDatum(TC_DATUM);
tft.setTextSize(2);
tft.drawString("SOS EMERGENCY", SCREEN_WIDTH / 2, 20);
// Warning text
tft.setTextColor(primary, bg);
tft.setTextSize(1);
tft.drawString("Press and hold for 3 seconds", SCREEN_WIDTH / 2, 60);
tft.drawString("to send emergency alert", SCREEN_WIDTH / 2, 75);
// Large SOS button
int btnRadius = 60;
int btnX = SCREEN_WIDTH / 2;
int btnY = SCREEN_HEIGHT / 2 + 20;
tft.fillCircle(btnX, btnY, btnRadius, accent);
tft.setTextColor(TFT_WHITE, accent);
tft.setTextSize(3);
tft.drawString("SOS", btnX, btnY);
// Countdown display
static uint32_t sosStartTime = 0;
if (touchPressed) {
int dist = sqrt((touchX - btnX) * (touchX - btnX) +
(touchY - btnY) * (touchY - btnY));
if (dist < btnRadius) {
if (sosStartTime == 0) {
sosStartTime = millis();
}
uint32_t elapsed = millis() - sosStartTime;
if (elapsed < 3000) {
int countdown = 3 - (elapsed / 1000);
tft.setTextColor(TFT_WHITE, accent);
tft.setTextSize(2);
tft.drawNumber(countdown, btnX, btnY + 30);
} else {
sendSOS();
sosStartTime = 0;
}
} else {
sosStartTime = 0;
}
} else {
sosStartTime = 0;
}
// Cancel button
tft.fillRoundRect(SCREEN_WIDTH / 2 - 40, SCREEN_HEIGHT - 50, 80, 30, 5, primary);
tft.setTextColor(bg, primary);
tft.setTextSize(1);
tft.drawString("Cancel", SCREEN_WIDTH / 2, SCREEN_HEIGHT - 35);
if (touchPressed && touchY > SCREEN_HEIGHT - 50 &&
touchX > SCREEN_WIDTH / 2 - 40 && touchX < SCREEN_WIDTH / 2 + 40) {
currentState = STATE_WATCH_FACE;
vibrate(30);
}
}
void drawFlashlight() {
static uint8_t mode = 0; // 0=normal, 1=slow, 2=fast, 3=SOS
static uint8_t brightness = 255;
static bool flashOn = false;
static uint32_t lastFlash = 0;
if (mode == 0) {
// Normal mode - steady on
digitalWrite(TFT_BL, HIGH);
analogWrite(TFT_BL, brightness);
tft.fillScreen(TFT_WHITE);
} else {
// Flashing modes
uint32_t interval = (mode == 1) ? 1000 : (mode == 2) ? 200 : 0;
if (mode == 3) {
// SOS mode: ... --- ... (3 short, 3 long, 3 short)
static uint8_t sosState = 0;
static uint32_t sosStart = 0;
if (millis() - sosStart > ((sosState % 2 == 0) ? 200 : 600)) {
sosState = (sosState + 1) % 6;
sosStart = millis();
flashOn = (sosState % 2 == 0);
}
} else if (millis() - lastFlash > interval) {
lastFlash = millis();
flashOn = !flashOn;
}
if (flashOn) {
tft.fillScreen(TFT_WHITE);
analogWrite(TFT_BL, brightness);
} else {
tft.fillScreen(TFT_BLACK);
analogWrite(TFT_BL, 0);
}
}
// Controls overlay (faded)
tft.fillRect(0, 0, SCREEN_WIDTH, 40, TFT_BLACK);
tft.setTextColor(TFT_WHITE, TFT_BLACK);
tft.setTextDatum(TC_DATUM);
tft.setTextSize(1);
const char* modes[] = {"Normal", "Slow", "Fast", "SOS"};
tft.drawString(modes[mode], SCREEN_WIDTH / 2, 10);
// Brightness bar
tft.drawRect(20, 25, SCREEN_WIDTH - 40, 8, TFT_WHITE);
tft.fillRect(22, 27, (SCREEN_WIDTH - 44) * brightness / 255, 4, TFT_WHITE);
// Touch to control
if (touchPressed) {
if (touchY < 40) {
// Change mode
mode = (mode + 1) % 4;
vibrate(30);
} else if (touchY > 200) {
// Adjust brightness
brightness = map(touchX, 0, SCREEN_WIDTH, 0, 255);
} else {
// Exit
currentState = STATE_WATCH_FACE;
setBrightness(settings.brightness);
}
}
}
void drawTimer() {
drawThemeBackground();
uint16_t bg = getColor(0);
uint16_t primary = getColor(1);
uint16_t accent = getColor(3);
static int timerMinutes = 5;
static int timerSeconds = 0;
static bool timerRunning = false;
static uint32_t timerStart = 0;
// Header
tft.setTextColor(primary, bg);
tft.setTextDatum(TC_DATUM);
tft.setTextSize(2);
tft.drawString("Timer", SCREEN_WIDTH / 2, 20);
// Time display
char timeStr[10];
if (timerRunning) {
uint32_t elapsed = (millis() - timerStart) / 1000;
int totalSeconds = timerMinutes * 60 + timerSeconds - elapsed;
if (totalSeconds <= 0) {
timerRunning = false;
vibrate(500);
}
sprintf(timeStr, "%02d:%02d", totalSeconds / 60, totalSeconds % 60);
} else {
sprintf(timeStr, "%02d:%02d", timerMinutes, timerSeconds);
}
tft.setTextColor(accent, bg);
tft.setTextSize(4);
tft.drawString(timeStr, SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2 - 20);
// Controls
int btnY = SCREEN_HEIGHT - 60;
int btnWidth = 60;
int gap = 20;
int startX = (SCREEN_WIDTH - (3 * btnWidth + 2 * gap)) / 2;
// - button
tft.fillRoundRect(startX, btnY, btnWidth, 40, 5, primary);
tft.setTextColor(bg, primary);
tft.setTextSize(2);
tft.drawString("-", startX + btnWidth / 2, btnY + 20);
// Start/Stop button
tft.fillRoundRect(startX + btnWidth + gap, btnY, btnWidth, 40, 5,
timerRunning ? TFT_RED : accent);
tft.setTextColor(TFT_WHITE, timerRunning ? TFT_RED : accent);
tft.setTextSize(1);
tft.drawString(timerRunning ? "Stop" : "Start",
startX + btnWidth + gap + btnWidth / 2, btnY + 20);
// + button
tft.fillRoundRect(startX + 2 * (btnWidth + gap), btnY, btnWidth, 40, 5, primary);
tft.setTextColor(bg, primary);
tft.setTextSize(2);
tft.drawString("+", startX + 2 * (btnWidth + gap) + btnWidth / 2, btnY + 20);
// Touch handling
if (touchPressed) {
if (touchY > btnY && touchY < btnY + 40) {
if (touchX < startX + btnWidth) {
if (timerMinutes > 0) timerMinutes--;
} else if (touchX < startX + 2 * btnWidth + gap) {
timerRunning = !timerRunning;
if (timerRunning) timerStart = millis();
} else {
if (timerMinutes < 60) timerMinutes++;
}
vibrate(30);
}
}
}
void drawAlarm() {
drawThemeBackground();
uint16_t bg = getColor(0);
uint16_t primary = getColor(1);
uint16_t accent = getColor(3);
static uint8_t alarmHour = 7;
static uint8_t alarmMinute = 0;
static bool alarmEnabled = true;
// Header
tft.setTextColor(primary, bg);
tft.setTextDatum(TC_DATUM);
tft.setTextSize(2);
tft.drawString("Alarm", SCREEN_WIDTH / 2, 20);
// Alarm time
char timeStr[10];
sprintf(timeStr, "%02d:%02d", alarmHour, alarmMinute);
tft.setTextColor(accent, bg);
tft.setTextSize(4);
tft.drawString(timeStr, SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2 - 20);
// AM/PM indicator
tft.setTextSize(1);
tft.setTextColor(primary, bg);
tft.drawString("AM", SCREEN_WIDTH / 2 + 80, SCREEN_HEIGHT / 2 - 30);
// On/Off switch
tft.fillRoundRect(SCREEN_WIDTH / 2 - 30, SCREEN_HEIGHT / 2 + 30, 60, 30, 15,
alarmEnabled ? accent : primary);
tft.setTextColor(TFT_WHITE, alarmEnabled ? accent : primary);
tft.setTextSize(1);
tft.drawString(alarmEnabled ? "ON" : "OFF", SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2 + 45);
// Set button
tft.fillRoundRect(SCREEN_WIDTH / 2 - 40, SCREEN_HEIGHT - 60, 80, 35, 5, primary);
tft.setTextColor(bg, primary);
tft.drawString("Set Alarm", SCREEN_WIDTH / 2, SCREEN_HEIGHT - 42);
// Touch handling
if (touchPressed) {
if (touchY > SCREEN_HEIGHT / 2 + 30 && touchY < SCREEN_HEIGHT / 2 + 60) {
alarmEnabled = !alarmEnabled;
vibrate(30);
} else if (touchY > SCREEN_HEIGHT - 60 && touchY < SCREEN_HEIGHT - 25) {
// Save alarm
vibrate(100);
currentState = STATE_WATCH_FACE;
} else if (touchY < SCREEN_HEIGHT / 2) {
// Adjust time (simplified - tap upper/lower half)
if (touchX < SCREEN_WIDTH / 2) {
alarmHour = (alarmHour + 1) % 24;
} else {
alarmMinute = (alarmMinute + 5) % 60;
}
vibrate(30);
}
}
// Check alarm
if (alarmEnabled && currentHour == alarmHour && currentMinute == alarmMinute &&
currentSecond == 0 && !settings.doNotDisturb) {
vibrate(1000);
// Show alarm screen
tft.fillScreen(accent);
tft.setTextColor(TFT_WHITE, accent);
tft.setTextSize(2);
tft.drawString("WAKE UP!", SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2);
delay(5000);
}
}
void drawStopwatch() {
drawThemeBackground();
uint16_t bg = getColor(0);
uint16_t primary = getColor(1);
uint16_t accent = getColor(3);
static uint32_t elapsedTime = 0;
static bool running = false;
static uint32_t startTime = 0;
// Header
tft.setTextColor(primary, bg);
tft.setTextDatum(TC_DATUM);
tft.setTextSize(2);
tft.drawString("Stopwatch", SCREEN_WIDTH / 2, 20);
// Time display
uint32_t displayTime = elapsedTime;
if (running) {
displayTime += (millis() - startTime);
}
uint32_t minutes = displayTime / 60000;
uint32_t seconds = (displayTime % 60000) / 1000;
uint32_t centis = (displayTime % 1000) / 10;
char timeStr[15];
sprintf(timeStr, "%02lu:%02lu.%02lu", minutes, seconds, centis);
tft.setTextColor(accent, bg);
tft.setTextSize(3);
tft.drawString(timeStr, SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2 - 20);
// Controls
int btnY = SCREEN_HEIGHT - 60;
int btnWidth = 70;
int gap = 30;
int startX = (SCREEN_WIDTH - (2 * btnWidth + gap)) / 2;
// Start/Stop
tft.fillRoundRect(startX, btnY, btnWidth, 40, 5, running ? TFT_RED : accent);
tft.setTextColor(TFT_WHITE, running ? TFT_RED : accent);
tft.setTextSize(1);
tft.drawString(running ? "Stop" : "Start", startX + btnWidth / 2, btnY + 20);
// Reset
tft.fillRoundRect(startX + btnWidth + gap, btnY, btnWidth, 40, 5, primary);
tft.setTextColor(bg, primary);
tft.drawString("Reset", startX + btnWidth + gap + btnWidth / 2, btnY + 20);
// Touch handling
if (touchPressed && touchY > btnY && touchY < btnY + 40) {
if (touchX < startX + btnWidth) {
if (running) {
elapsedTime += (millis() - startTime);
running = false;
} else {
startTime = millis();
running = true;
}
} else {
elapsedTime = 0;
running = false;
}
vibrate(30);
}
}
void drawBreathing() {
drawThemeBackground();
uint16_t bg = getColor(0);
uint16_t primary = getColor(1);
uint16_t accent = getColor(3);
uint16_t secondary = getColor(2);
static uint8_t breathState = 0; // 0=inhale, 1=hold, 2=exhale, 3=hold
static uint32_t breathStart = 0;
static uint8_t breathCycle = 0;
uint32_t breathDuration = 4000; // 4 seconds per phase
uint32_t elapsed = millis() - breathStart;
if (elapsed > breathDuration) {
breathState = (breathState + 1) % 4;
breathStart = millis();
if (breathState == 0) breathCycle++;
}
// Header
tft.setTextColor(primary, bg);
tft.setTextDatum(TC_DATUM);
tft.setTextSize(2);
tft.drawString("Breathe", SCREEN_WIDTH / 2, 20);
// Expanding/contracting circle
int centerX = SCREEN_WIDTH / 2;
int centerY = SCREEN_HEIGHT / 2;
int maxRadius = 80;
int minRadius = 30;
float progress = (float)elapsed / breathDuration;
int radius;
const char* states[] = {"Inhale", "Hold", "Exhale", "Hold"};
if (breathState == 0) { // Inhale - expand
radius = minRadius + (maxRadius - minRadius) * progress;
} else if (breathState == 2) { // Exhale - contract
radius = maxRadius - (maxRadius - minRadius) * progress;
} else { // Hold
radius = (breathState == 1) ? maxRadius : minRadius;
}
// Draw animated circle
for (int r = 5; r <= radius; r += 5) {
uint16_t color = (r > radius - 10) ? accent : secondary;
tft.drawCircle(centerX, centerY, r, color);
}
// State text
tft.setTextColor(accent, bg);
tft.setTextSize(2);
tft.drawString(states[breathState], centerX, centerY);
// Cycle counter
tft.setTextColor(primary, bg);
tft.setTextSize(1);
char cycleStr[20];
sprintf(cycleStr, "Cycle: %d/5", breathCycle);
tft.drawString(cycleStr, centerX, SCREEN_HEIGHT - 40);
// Exit button
tft.fillRoundRect(SCREEN_WIDTH / 2 - 30, SCREEN_HEIGHT - 30, 60, 25, 5, primary);
tft.setTextColor(bg, primary);
tft.drawString("Done", SCREEN_WIDTH / 2, SCREEN_HEIGHT - 17);
if (touchPressed && touchY > SCREEN_HEIGHT - 30) {
currentState = STATE_WATCH_FACE;
vibrate(30);
}
// Auto-exit after 5 cycles
if (breathCycle >= 5) {
currentState = STATE_WATCH_FACE;
vibrate(100);
}
}
void drawFindPhone() {
drawThemeBackground();
uint16_t bg = getColor(0);
uint16_t primary = getColor(1);
uint16_t accent = getColor(3);
uint16_t secondary = getColor(2);
static bool ringing = false;
static uint32_t ringStart = 0;
// Header
tft.setTextColor(primary, bg);
tft.setTextDatum(TC_DATUM);
tft.setTextSize(2);
tft.drawString("Find My Phone", SCREEN_WIDTH / 2, 30);
// Phone icon (simplified)
int phoneX = SCREEN_WIDTH / 2;
int phoneY = SCREEN_HEIGHT / 2 - 20;
if (ringing && millis() - ringStart > 500) {
// Pulse effect
int pulse = (millis() / 100) % 10;
tft.drawCircle(phoneX, phoneY, 40 + pulse, accent);
}
// Phone rectangle
tft.fillRoundRect(phoneX - 20, phoneY - 35, 40, 70, 5, primary);
tft.fillRect(phoneX - 15, phoneY - 25, 30, 45, bg);
// Ring button
int btnY = SCREEN_HEIGHT - 70;
tft.fillRoundRect(SCREEN_WIDTH / 2 - 60, btnY, 120, 45, 8,
ringing ? TFT_RED : accent);
tft.setTextColor(TFT_WHITE, ringing ? TFT_RED : accent);
tft.setTextSize(1);
tft.drawString(ringing ? "Stop Ringing" : "Ring Phone", SCREEN_WIDTH / 2, btnY + 22);
// Status
tft.setTextColor(secondary, bg);
tft.setTextSize(1);
tft.drawString(ringing ? "Phone is ringing..." : "Press to ring phone",
SCREEN_WIDTH / 2, SCREEN_HEIGHT - 110);
// Touch handling
if (touchPressed && touchY > btnY && touchY < btnY + 45) {
ringing = !ringing;
if (ringing) {
ringStart = millis();
sendFindPhone();
}
vibrate(30);
}
// Auto stop after 30 seconds
if (ringing && millis() - ringStart > 30000) {
ringing = false;
}
// Close button
tft.fillRoundRect(SCREEN_WIDTH / 2 - 40, SCREEN_HEIGHT - 25, 80, 20, 5, primary);
tft.setTextColor(bg, primary);
tft.setTextSize(1);
tft.drawString("Close", SCREEN_WIDTH / 2, SCREEN_HEIGHT - 15);
if (touchPressed && touchY > SCREEN_HEIGHT - 25) {
currentState = STATE_WATCH_FACE;
ringing = false;
vibrate(30);
}
}
void drawGames() {
drawThemeBackground();
uint16_t bg = getColor(0);
uint16_t primary = getColor(1);
uint16_t secondary = getColor(2);
if (gameActive) {
playGame();
return;
}
// Header
tft.setTextColor(primary, bg);
tft.setTextDatum(TC_DATUM);
tft.setTextSize(2);
tft.drawString("Games", SCREEN_WIDTH / 2, 20);
// Game selection grid
const char* games[] = {
"Snake", "Blocks", "Runner",
"TicTacToe", "Infinity", "Maze"
};
int startY = 60;
int gameSize = 60;
int gap = 15;
int cols = 2;
int startX = (SCREEN_WIDTH - (cols * gameSize + (cols - 1) * gap)) / 2;
for (int i = 0; i < 6; i++) {
int col = i % cols;
int row = i / cols;
int x = startX + col * (gameSize + gap);
int y = startY + row * (gameSize + gap + 10);
tft.fillRoundRect(x, y, gameSize, gameSize, 8, secondary);
tft.setTextColor(primary, secondary);
tft.setTextDatum(MC_DATUM);
tft.setTextSize(1);
tft.drawString(games[i], x + gameSize / 2, y + gameSize / 2);
}
// Touch handling
if (touchPressed) {
int col = (touchX - startX) / (gameSize + gap);
int row = (touchY - startY) / (gameSize + gap + 10);
if (col >= 0 && col < cols && row >= 0 && row < 3) {
int gameIndex = row * cols + col;
if (gameIndex < 6) {
currentGame = (GameType)gameIndex;
gameActive = true;
gameScore = 0;
vibrate(50);
}
}
}
}
void playGame() {
switch (currentGame) {
case GAME_SNAKE:
drawGameSnake();
break;
case GAME_BLOCK_PUZZLE:
drawGameBlockPuzzle();
break;
case GAME_RUNNER:
drawGameRunner();
break;
case GAME_TIC_TAC_TOE:
drawGameTicTacToe();
break;
case GAME_INFINITY_LOOP:
drawGameInfinityLoop();
break;
case GAME_MAZE:
drawGameMaze();
break;
}
handleGameInput();
}
// ==================== GAMES IMPLEMENTATION ====================
void drawGameSnake() {
static int8_t snakeX[100], snakeY[100];
static uint8_t snakeLength = 3;
static int8_t foodX, foodY;
static int8_t dirX = 1, dirY = 0;
static uint32_t lastMove = 0;
static bool initialized = false;
const int gridSize = 10;
const int cellSize = SCREEN_WIDTH / gridSize;
const int gridHeight = (SCREEN_HEIGHT - 40) / cellSize;
if (!initialized) {
for (int i = 0; i < snakeLength; i++) {
snakeX[i] = 5 - i;
snakeY[i] = 5;
}
foodX = random(1, gridSize - 1);
foodY = random(1, gridHeight - 1);
initialized = true;
}
// Move snake
if (millis() - lastMove > 200) {
lastMove = millis();
// Shift body
for (int i = snakeLength - 1; i > 0; i--) {
snakeX[i] = snakeX[i - 1];
snakeY[i] = snakeY[i - 1];
}
// Move head
snakeX[0] += dirX;
snakeY[0] += dirY;
// Wrap around
if (snakeX[0] < 0) snakeX[0] = gridSize - 1;
if (snakeX[0] >= gridSize) snakeX[0] = 0;
if (snakeY[0] < 0) snakeY[0] = gridHeight - 1;
if (snakeY[0] >= gridHeight) snakeY[0] = 0;
// Check food collision
if (snakeX[0] == foodX && snakeY[0] == foodY) {
if (snakeLength < 100) snakeLength++;
gameScore += 10;
foodX = random(1, gridSize - 1);
foodY = random(1, gridHeight - 1);
vibrate(50);
}
// Check self collision
for (int i = 1; i < snakeLength; i++) {
if (snakeX[0] == snakeX[i] && snakeY[0] == snakeY[i]) {
// Game over
gameActive = false;
initialized = false;
snakeLength = 3;
tft.fillScreen(TFT_RED);
tft.setTextColor(TFT_WHITE, TFT_RED);
tft.setTextDatum(MC_DATUM);
tft.drawString("Game Over!", SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2);
tft.drawNumber(gameScore, SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2 + 30);
delay(2000);
return;
}
}
}
// Draw
tft.fillScreen(getColor(0));
// Draw food
tft.fillCircle(foodX * cellSize + cellSize / 2,
foodY * cellSize + cellSize / 2 + 40,
cellSize / 2 - 2, TFT_RED);
// Draw snake
for (int i = 0; i < snakeLength; i++) {
uint16_t color = (i == 0) ? getColor(3) : getColor(2);
tft.fillRect(snakeX[i] * cellSize + 1,
snakeY[i] * cellSize + 1 + 40,
cellSize - 2, cellSize - 2, color);
}
// Score
tft.setTextColor(getColor(1), getColor(0));
tft.setTextDatum(TL_DATUM);
tft.setTextSize(1);
tft.drawString("Score:", 10, 10);
tft.drawNumber(gameScore, 50, 10);
// Exit button
tft.fillRoundRect(SCREEN_WIDTH - 50, 5, 40, 25, 5, getColor(2));
tft.setTextColor(getColor(1), getColor(2));
tft.setTextDatum(MC_DATUM);
tft.drawString("X", SCREEN_WIDTH - 30, 17);
}
void drawGameBlockPuzzle() {
static uint8_t grid[10][20];
static uint8_t currentPiece[4][4];
static int8_t pieceX = 3, pieceY = 0;
static uint32_t lastDrop = 0;
static bool initialized = false;
const int cellSize = 12;
const int gridW = 10;
const int gridH = 15;
const int offsetX = (SCREEN_WIDTH - gridW * cellSize) / 2;
const int offsetY = 50;
if (!initialized) {
memset(grid, 0, sizeof(grid));
for (int y = 0; y < 4; y++) {
for (int x = 0; x < 4; x++) {
currentPiece[x][y] = random(0, 2);
}
}
pieceX = 3;
pieceY = 0;
initialized = true;
}
if (millis() - lastDrop > 1000) {
lastDrop = millis();
pieceY++;
if (pieceY + 4 > gridH || checkBlockCollision()) {
pieceY--;
for (int y = 0; y < 4; y++) {
for (int x = 0; x < 4; x++) {
if (currentPiece[x][y] && pieceX + x >= 0 && pieceX + x < gridW) {
grid[pieceX + x][pieceY + y] = 1;
}
}
}
for (int y = gridH - 1; y >= 0; y--) {
bool lineFull = true;
for (int x = 0; x < gridW; x++) {
if (!grid[x][y]) lineFull = false;
}
if (lineFull) {
for (int yy = y; yy > 0; yy--) {
for (int x = 0; x < gridW; x++) {
grid[x][yy] = grid[x][yy - 1];
}
}
gameScore += 100;
vibrate(100);
}
}
for (int y = 0; y < 4; y++) {
for (int x = 0; x < 4; x++) {
currentPiece[x][y] = random(0, 2);
}
}
pieceX = 3;
pieceY = 0;
if (checkBlockCollision()) {
gameActive = false;
initialized = false;
}
}
}
tft.fillScreen(getColor(0));
for (int y = 0; y < gridH; y++) {
for (int x = 0; x < gridW; x++) {
if (grid[x][y]) {
tft.fillRect(offsetX + x * cellSize, offsetY + y * cellSize,
cellSize - 1, cellSize - 1, getColor(3));
}
}
}
for (int y = 0; y < 4; y++) {
for (int x = 0; x < 4; x++) {
if (currentPiece[x][y]) {
tft.fillRect(offsetX + (pieceX + x) * cellSize,
offsetY + (pieceY + y) * cellSize,
cellSize - 1, cellSize - 1, getColor(2));
}
}
}
tft.setTextColor(getColor(1), getColor(0));
tft.setTextDatum(TC_DATUM);
tft.setTextSize(1);
tft.drawString("Block Puzzle", SCREEN_WIDTH / 2, 10);
tft.drawNumber(gameScore, SCREEN_WIDTH / 2, 30);
tft.fillRoundRect(SCREEN_WIDTH - 40, 5, 30, 20, 3, getColor(2));
tft.setTextColor(getColor(1), getColor(2));
tft.setTextDatum(MC_DATUM);
tft.drawString("X", SCREEN_WIDTH - 25, 15);
}
bool checkBlockCollision() {
return false;
}
void drawGameRunner() {
static int8_t playerY = 0;
static float playerVelY = 0;
static uint8_t obstacles[3];
static uint8_t obstacleX[3];
static uint32_t lastUpdate = 0;
static bool initialized = false;
const int groundY = SCREEN_HEIGHT - 60;
const int gravity = 1;
const int jumpPower = -8;
if (!initialized) {
for (int i = 0; i < 3; i++) {
obstacles[i] = random(0, 2);
obstacleX[i] = 100 + i * 80;
}
playerY = 0;
playerVelY = 0;
initialized = true;
}
if (millis() - lastUpdate > 20) {
lastUpdate = millis();
playerVelY += gravity;
playerY += playerVelY;
if (playerY > 0) {
playerY = 0;
playerVelY = 0;
}
for (int i = 0; i < 3; i++) {
obstacleX[i] -= 2;
if (obstacleX[i] < -10) {
obstacleX[i] = SCREEN_WIDTH + random(20, 100);
obstacles[i] = random(0, 2);
gameScore++;
}
}
for (int i = 0; i < 3; i++) {
if (obstacles[i] && obstacleX[i] > 20 && obstacleX[i] < 50 && playerY > -20) {
gameActive = false;
initialized = false;
tft.fillScreen(TFT_RED);
tft.setTextColor(TFT_WHITE, TFT_RED);
tft.setTextDatum(MC_DATUM);
tft.drawString("Crashed!", SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2);
delay(1500);
return;
}
}
}
tft.fillScreen(getColor(0));
tft.fillRect(0, groundY, SCREEN_WIDTH, 60, getColor(2));
int playerScreenY = groundY + playerY - 20;
tft.fillRect(30, playerScreenY, 20, 20, getColor(3));
for (int i = 0; i < 3; i++) {
if (obstacles[i]) {
tft.fillRect(obstacleX[i], groundY - 20, 15, 20, TFT_RED);
}
}
tft.setTextColor(getColor(1), getColor(0));
tft.setTextDatum(TL_DATUM);
tft.setTextSize(1);
tft.drawNumber(gameScore, 10, 10);
tft.setTextColor(getColor(2), getColor(0));
tft.setTextDatum(BC_DATUM);
tft.drawString("Tap to Jump", SCREEN_WIDTH / 2, SCREEN_HEIGHT - 10);
}
void drawGameTicTacToe() {
static int8_t board[3][3];
static uint8_t currentPlayer = 1;
static bool gameOver = false;
static uint8_t winner = 0;
const int cellSize = 60;
const int startX = (SCREEN_WIDTH - 3 * cellSize) / 2;
const int startY = 60;
if (!gameOver) {
for (int i = 0; i < 3; i++) {
if (board[i][0] == board[i][1] && board[i][1] == board[i][2] && board[i][0] != 0) {
gameOver = true;
winner = board[i][0];
}
if (board[0][i] == board[1][i] && board[1][i] == board[2][i] && board[0][i] != 0) {
gameOver = true;
winner = board[0][i];
}
}
if (board[0][0] == board[1][1] && board[1][1] == board[2][2] && board[0][0] != 0) {
gameOver = true;
winner = board[0][0];
}
if (board[0][2] == board[1][1] && board[1][1] == board[2][0] && board[0][2] != 0) {
gameOver = true;
winner = board[0][2];
}
bool full = true;
for (int y = 0; y < 3; y++) {
for (int x = 0; x < 3; x++) {
if (board[x][y] == 0) full = false;
}
}
if (full && !gameOver) {
gameOver = true;
winner = 0;
}
}
tft.fillScreen(getColor(0));
for (int i = 1; i < 3; i++) {
tft.fillRect(startX + i * cellSize - 2, startY, 4, 3 * cellSize, getColor(1));
tft.fillRect(startX, startY + i * cellSize - 2, 3 * cellSize, 4, getColor(1));
}
for (int y = 0; y < 3; y++) {
for (int x = 0; x < 3; x++) {
int cx = startX + x * cellSize + cellSize / 2;
int cy = startY + y * cellSize + cellSize / 2;
if (board[x][y] == 1) {
tft.drawLine(cx - 15, cy - 15, cx + 15, cy + 15, getColor(3));
tft.drawLine(cx + 15, cy - 15, cx - 15, cy + 15, getColor(3));
} else if (board[x][y] == 2) {
tft.drawCircle(cx, cy, 15, getColor(2));
}
}
}
tft.setTextColor(getColor(1), getColor(0));
tft.setTextDatum(TC_DATUM);
tft.setTextSize(1);
if (gameOver) {
if (winner == 1) {
tft.drawString("X Wins!", SCREEN_WIDTH / 2, 30);
gameScore += 50;
} else if (winner == 2) {
tft.drawString("O Wins!", SCREEN_WIDTH / 2, 30);
} else {
tft.drawString("Draw!", SCREEN_WIDTH / 2, 30);
}
tft.fillRoundRect(SCREEN_WIDTH / 2 - 40, SCREEN_HEIGHT - 40, 80, 30, 5, getColor(2));
tft.setTextColor(getColor(1), getColor(2));
tft.drawString("New Game", SCREEN_WIDTH / 2, SCREEN_HEIGHT - 25);
} else {
char status[20];
sprintf(status, "Player %s turn", currentPlayer == 1 ? "X" : "O");
tft.drawString(status, SCREEN_WIDTH / 2, 30);
}
if (touchPressed && !gameOver) {
int col = (touchX - startX) / cellSize;
int row = (touchY - startY) / cellSize;
if (col >= 0 && col < 3 && row >= 0 && row < 3 && board[col][row] == 0) {
board[col][row] = currentPlayer;
currentPlayer = (currentPlayer == 1) ? 2 : 1;
vibrate(30);
}
}
if (gameOver && touchPressed && touchY > SCREEN_HEIGHT - 40) {
memset(board, 0, sizeof(board));
currentPlayer = 1;
gameOver = false;
winner = 0;
vibrate(30);
}
}
void drawGameInfinityLoop() {
static uint8_t grid[4][4];
static uint8_t rotations[4][4];
static bool initialized = false;
if (!initialized) {
for (int y = 0; y < 4; y++) {
for (int x = 0; x < 4; x++) {
grid[x][y] = random(1, 4);
rotations[x][y] = random(0, 4);
}
}
initialized = true;
}
const int cellSize = 45;
const int startX = (SCREEN_WIDTH - 4 * cellSize) / 2;
const int startY = 50;
tft.fillScreen(getColor(0));
tft.setTextColor(getColor(1), getColor(0));
tft.setTextDatum(TC_DATUM);
tft.setTextSize(1);
tft.drawString("Infinity Loop", SCREEN_WIDTH / 2, 20);
for (int y = 0; y < 4; y++) {
for (int x = 0; x < 4; x++) {
int cx = startX + x * cellSize + cellSize / 2;
int cy = startY + y * cellSize + cellSize / 2;
uint16_t color = getColor(2);
if (isLoopConnected(x, y)) {
color = getColor(3);
}
tft.fillRoundRect(startX + x * cellSize + 2, startY + y * cellSize + 2,
cellSize - 4, cellSize - 4, 5, color);
if (grid[x][y] == 1) {
} else if (grid[x][y] == 2) {
tft.fillRect(cx - 5, cy - 20, 10, 40, getColor(1));
} else {
tft.fillRect(cx - 5, cy - 20, 10, 40, getColor(1));
tft.fillRect(cx - 20, cy - 5, 40, 10, getColor(1));
}
}
}
if (touchPressed) {
int col = (touchX - startX) / cellSize;
int row = (touchY - startY) / cellSize;
if (col >= 0 && col < 4 && row >= 0 && row < 4) {
rotations[col][row] = (rotations[col][row] + 1) % 4;
vibrate(30);
if (isPuzzleSolved()) {
gameScore += 200;
tft.fillScreen(getColor(3));
tft.setTextColor(TFT_WHITE, getColor(3));
tft.setTextDatum(MC_DATUM);
tft.drawString("Solved!", SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2);
vibrate(200);
delay(1500);
initialized = false;
}
}
}
tft.fillRoundRect(SCREEN_WIDTH - 40, 5, 30, 20, 3, getColor(2));
tft.setTextColor(getColor(1), getColor(2));
tft.setTextDatum(MC_DATUM);
tft.drawString("X", SCREEN_WIDTH - 25, 15);
if (touchPressed && touchX > SCREEN_WIDTH - 40 && touchY < 25) {
gameActive = false;
initialized = false;
}
}
bool isLoopConnected(int x, int y) {
return false;
}
bool isPuzzleSolved() {
return false;
}
void drawGameMaze() {
static bool initialized = false;
const int cellSize = 15;
const int mazeW = 15;
const int mazeH = 15;
const int offsetX = (SCREEN_WIDTH - mazeW * cellSize) / 2;
const int offsetY = 50;
if (!initialized) {
for (int y = 0; y < mazeH; y++) {
for (int x = 0; x < mazeW; x++) {
if (x == 0 || x == mazeW - 1 || y == 0 || y == mazeH - 1) {
maze[x][y] = 1;
} else if (random(0, 4) == 0) {
maze[x][y] = 1;
} else {
maze[x][y] = 0;
}
}
}
maze[1][1] = 0;
maze[mazeW - 2][mazeH - 2] = 0;
playerX = 1;
playerY = 1;
initialized = true;
}
tft.fillScreen(getColor(0));
for (int y = 0; y < mazeH; y++) {
for (int x = 0; x < mazeW; x++) {
if (maze[x][y]) {
tft.fillRect(offsetX + x * cellSize, offsetY + y * cellSize,
cellSize, cellSize, getColor(2));
}
}
}
tft.fillCircle(offsetX + playerX * cellSize + cellSize / 2,
offsetY + playerY * cellSize + cellSize / 2,
cellSize / 2 - 2, getColor(3));
tft.fillCircle(offsetX + (mazeW - 2) * cellSize + cellSize / 2,
offsetY + (mazeH - 2) * cellSize + cellSize / 2,
cellSize / 2 - 2, TFT_GREEN);
if (playerX == mazeW - 2 && playerY == mazeH - 2) {
gameScore += 300;
tft.fillScreen(TFT_GREEN);
tft.setTextColor(TFT_BLACK, TFT_GREEN);
tft.setTextDatum(MC_DATUM);
tft.drawString("You Win!", SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2);
vibrate(300);
delay(2000);
initialized = false;
gameActive = false;
return;
}
tft.fillRoundRect(SCREEN_WIDTH - 40, 5, 30, 20, 3, getColor(2));
tft.setTextColor(getColor(1), getColor(2));
tft.setTextDatum(MC_DATUM);
tft.drawString("X", SCREEN_WIDTH - 25, 15);
if (touchPressed && touchX > SCREEN_WIDTH - 40 && touchY < 25) {
gameActive = false;
initialized = false;
}
}
void handleGameInput() {
if (!touchPressed) return;
if (touchX > SCREEN_WIDTH - 50 && touchY < 30) {
gameActive = false;
vibrate(30);
return;
}
switch (currentGame) {
case GAME_SNAKE:
static int16_t lastTouchX = 0, lastTouchY = 0;
static bool firstTouch = true;
if (firstTouch) {
lastTouchX = touchX;
lastTouchY = touchY;
firstTouch = false;
} else {
int16_t dx = touchX - lastTouchX;
int16_t dy = touchY - lastTouchY;
if (abs(dx) > 20 || abs(dy) > 20) {
if (abs(dx) > abs(dy)) {
if (dx > 0 && dirX != -1) { dirX = 1; dirY = 0; }
else if (dx < 0 && dirX != 1) { dirX = -1; dirY = 0; }
} else {
if (dy > 0 && dirY != -1) { dirX = 0; dirY = 1; }
else if (dy < 0 && dirY != 1) { dirX = 0; dirY = -1; }
}
lastTouchX = touchX;
lastTouchY = touchY;
}
}
break;
case GAME_BLOCK_PUZZLE:
if (touchY > 200) {
if (touchX < SCREEN_WIDTH / 2) {
// Move left
} else {
// Move right
}
}
break;
case GAME_RUNNER:
if (touchY > 100 && touchY < 200) {
playerVelY = -8; // jumpPower
}
break;
case GAME_TIC_TAC_TOE:
break;
case GAME_INFINITY_LOOP:
break;
case GAME_MAZE:
{
int centerX = SCREEN_WIDTH / 2;
int centerY = SCREEN_HEIGHT / 2;
if (touchX < centerX && playerX > 0 && maze[playerX - 1][playerY] == 0) {
playerX--;
vibrate(20);
} else if (touchX > centerX && playerX < 14 && maze[playerX + 1][playerY] == 0) {
playerX++;
vibrate(20);
} else if (touchY < centerY && playerY > 0 && maze[playerX][playerY - 1] == 0) {
playerY--;
vibrate(20);
} else if (touchY > centerY && playerY < 14 && maze[playerX][playerY + 1] == 0) {
playerY++;
vibrate(20);
}
}
break;
}
}
// ==================== UTILITY FUNCTIONS ====================
void handleBluetoothData() {
String data = ble.readStringUntil('\n');
data.trim();
if (data.startsWith("HEALTH:")) {
// Parse health data from phone (if needed)
} else if (data.startsWith("NOTIFICATION:")) {
int firstColon = data.indexOf(':');
int secondColon = data.indexOf(':', firstColon + 1);
int thirdColon = data.indexOf(':', secondColon + 1);
if (firstColon > 0 && secondColon > 0 && thirdColon > 0) {
String app = data.substring(firstColon + 1, secondColon);
String title = data.substring(secondColon + 1, thirdColon);
String message = data.substring(thirdColon + 1);
receiveNotification(app.c_str(), title.c_str(), message.c_str());
}
} else if (data == "FIND_PHONE_ACK") {
findPhoneActive = false;
}
}
void saveSettings() {
// Save to EEPROM or NVS (simplified)
}
void loadSettings() {
settings.watchFace = FACE_ANALOG_CLASSIC;
settings.theme = THEME_MIDNIGHT;
settings.brightness = 128;
settings.vibrationEnabled = true;
settings.doNotDisturb = false;
settings.sleepMode = false;
settings.pinLock = false;
settings.notificationEnabled = true;
settings.volume = 50;
strcpy(settings.pin, "0000");
}
void setBrightness(uint8_t level) {
analogWrite(TFT_BL, level);
}
void vibrate(uint16_t duration) {
if (settings.vibrationEnabled) {
// Use GPIO for vibration motor (if connected)
// For now, just a delay simulation
delay(duration);
}
}
void showAOD() {
aodActive = true;
// Dim display to minimum
setBrightness(5);
// Show minimal watch face
drawWatchFace();
}
void sendSOS() {
// Send emergency alert via Bluetooth
if (bluetoothConnected) {
ble.println("SOS_ALERT");
ble.println("EMERGENCY: Help needed!");
}
// Show confirmation
tft.fillScreen(TFT_RED);
tft.setTextColor(TFT_WHITE, TFT_RED);
tft.setTextDatum(MC_DATUM);
tft.setTextSize(2);
tft.drawString("SOS SENT!", SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2);
vibrate(1000);
delay(3000);
currentState = STATE_WATCH_FACE;
}
void sendFindPhone() {
if (bluetoothConnected) {
ble.println("FIND_PHONE");
findPhoneActive = true;
}
}
void connectToPhone() {
// BLE connection status - ble is connected when a central device connects
// BleSerial manages connection internally
bluetoothConnected = true;
}
void receiveNotification(const char* app, const char* title, const char* message) {
if (notificationCount < 20) {
strncpy(notifications[notificationCount].app, app, 31);
strncpy(notifications[notificationCount].title, title, 63);
strncpy(notifications[notificationCount].message, message, 255);
notifications[notificationCount].timestamp = millis() / 1000;
notifications[notificationCount].read = false;
notificationCount++;
// Vibrate for notification
if (!settings.doNotDisturb) {
vibrate(200);
}
}
}
float calculateDistance(uint16_t steps) {
// Average step length ~0.7 meters
return steps * 0.7 / 1000.0; // Return km
}
uint16_t calculateCalories(uint16_t steps, uint8_t heartRate) {
// Simplified calorie calculation
// Base: 0.04 kcal per step, adjusted by heart rate
float multiplier = 1.0 + (heartRate - 60) / 100.0;
return (uint16_t)(steps * 0.04 * multiplier);
}
void breathingExercise() {
// Handled in drawBreathing()
}
void updateWeather() {
// Request weather update from phone
if (bluetoothConnected) {
ble.println("GET_WEATHER");
}
}
void updateMusicControl() {
// Handled in drawWeatherMusic()
}
int8_t getBatteryLevel() {
// Read battery voltage (simplified)
// In real implementation, use ADC to read battery voltage
// ESP32-S3 has built-in battery monitoring
return 75; // Placeholder
}
void showAbout() {
tft.fillScreen(getColor(0));
uint16_t primary = getColor(1);
uint16_t accent = getColor(3);
tft.setTextColor(primary, getColor(0));
tft.setTextDatum(TC_DATUM);
tft.setTextSize(2);
tft.drawString("About", SCREEN_WIDTH / 2, 30);
tft.setTextSize(1);
tft.drawString("Vital Watch S1", SCREEN_WIDTH / 2, 70);
tft.drawString("SmartWatch Pro", SCREEN_WIDTH / 2, 90);
tft.drawString("Version 1.0", SCREEN_WIDTH / 2, 110);
tft.drawString("ESP32-S3", SCREEN_WIDTH / 2, 140);
tft.drawString("240x280 LCD", SCREEN_WIDTH / 2, 160);
tft.drawString("Bluetooth 5.0", SCREEN_WIDTH / 2, 180);
tft.setTextColor(accent, getColor(0));
tft.drawString("Tap to close", SCREEN_WIDTH / 2, SCREEN_HEIGHT - 40);
// Wait for touch
while (!touchPressed) { delay(10); }
while (touchPressed) { delay(10); }
currentState = STATE_WATCH_FACE;
}
void showPowerMenu() {
// Simplified power menu
tft.fillScreen(getColor(0));
uint16_t primary = getColor(1);
uint16_t accent = getColor(3);
tft.setTextColor(primary, getColor(0));
tft.setTextDatum(TC_DATUM);
tft.setTextSize(2);
tft.drawString("Power", SCREEN_WIDTH / 2, 30);
// Power off button
tft.fillRoundRect(SCREEN_WIDTH / 2 - 60, 80, 120, 50, 8, TFT_RED);
tft.setTextColor(TFT_WHITE, TFT_RED);
tft.setTextSize(1);
tft.drawString("Power Off", SCREEN_WIDTH / 2, 105);
// Restart button
tft.fillRoundRect(SCREEN_WIDTH / 2 - 60, 150, 120, 50, 8, accent);
tft.setTextColor(getColor(0), accent);
tft.drawString("Restart", SCREEN_WIDTH / 2, 175);
// Cancel button
tft.fillRoundRect(SCREEN_WIDTH / 2 - 60, 220, 120, 40, 8, primary);
tft.setTextColor(getColor(0), primary);
tft.drawString("Cancel", SCREEN_WIDTH / 2, 240);
// Wait for touch
while (true) {
readTouch();
if (touchPressed) {
if (touchY > 80 && touchY < 130) {
// Power off
tft.fillScreen(TFT_BLACK);
setBrightness(0);
delay(1000);
esp_deep_sleep_start();
} else if (touchY > 150 && touchY < 200) {
// Restart
ESP.restart();
} else if (touchY > 220) {
currentState = STATE_SETTINGS;
return;
}
}
delay(10);
}
}
// Array of accent colors for notifications
uint16_t accentColors[] = {
TFT_RED, TFT_GREEN, TFT_BLUE, TFT_YELLOW, TFT_CYAN, TFT_MAGENTA
};
// Global for flashlight state
bool flashlightActive = false;
// Game globals
int8_t dirX = 1, dirY = 0;
float playerVelY = 0;
uint8_t maze[15][15];
int8_t playerX = 1, playerY = 1;