#include <Wire.h>
#include <WiFi.h>
#include <DHT.h>
#include <PubSubClient.h>
#include <Arduino.h>
#include <MPU6050_tockn.h>
#include <ArduinoJson.h>
#include <OneWire.h>
#include <DallasTemperature.h>
#include <SPI.h>
#include <Adafruit_GFX.h>
#include <Adafruit_ILI9341.h>
// TFT Display Configuration
#define TFT_DC 4
#define TFT_CS 5
Adafruit_ILI9341 tft = Adafruit_ILI9341(TFT_CS, TFT_DC);
// Sensor Pin Definitions
#define PULSE_PIN 35
#define DHT_PIN 15
#define DHT_TYPE DHT22
#define DS18B20_PIN 2
#define BREATHING_PIN 34 // PM for breathing rate simulation
#define HRV_PIN 32 // PM for HRV simulation
#define SPEED_PIN 33 // PM for speed control simulation
#define ECG_P_WAVE_PIN 37 // ECG P wave amplitude control (changed from 25 to 37 - ADC1_CH1)
#define ECG_RHYTHM_PIN 38 // ECG rhythm regularity control (changed from 26 to 38 - ADC1_CH2)
#define ECG_QRS_PIN 36 // ECG QRS complex control (36 is ADC1_CH0)
#define PULSE_SENSOR_ADC_MAX 2703.0
#define BUZZER_PIN 27
#define EMERGENCY_BUTTON_PIN 13
#define POWER_SWITCH_PIN 16 // Switch to turn entire device on/off
#define DISPLAY_BUTTON_PIN 17 // Button to toggle display on/off
// LED Pins for Status Indication
#define RED_LED_PIN 12
#define GREEN_LED_PIN 14
// Device Control Variables
bool devicePoweredOn = true; // false to start with device OFF
bool displayEnabled = true; // false to start with display OFF
// Emergency Button Variables
bool lastButtonState = HIGH; // not pressed
unsigned long lastDebounceTime = 0;
const unsigned long DEBOUNCE_DELAY = 50;
// Power Switch
bool lastPowerSwitchState = HIGH;
unsigned long lastPowerDebounceTime = 0;
// Display Button
bool lastDisplayButtonState = HIGH;
unsigned long lastDisplayDebounceTime = 0;
// Display OFF breathing effect variables
unsigned long lastBreathingUpdate = 0;
const unsigned long BREATHING_INTERVAL = 50; // Update every 50ms
float breathingPhase = 0.0; // Phase for breathing animation (0 to 2*PI)
const float BREATHING_SPEED = 0.05; // Speed of breathing animation
// Emergency Button Feedback Variables
bool emergencyButtonPressed = false; // Flag for UI feedback
unsigned long emergencyButtonPressTime = 0; // When button was pressed
const unsigned long EMERGENCY_FEEDBACK_DURATION = 3000; // Show feedback for 3 seconds
// Emergency alert state tracking
bool emergencyAlertSent = false; // Prevents multiple alerts per button press
unsigned long emergencyAlertTime = 0; // When last emergency alert was sent
// WiFi and MQTT Configuration
#define MQTT_SERVER "broker.emqx.io"
#define MQTT_PORT 1883
const char *ssid = "Wokwi-GUEST";
const char *password = "";
// MQTT Topics - Single JSON approach (Real Wokwi Device)
#define MQTT_TOPIC_BASE "cyclists/wokwi_device"
#define MQTT_TOPIC_DATA MQTT_TOPIC_BASE "/sensors/data" // All sensor data in one JSON
#define MQTT_TOPIC_HEALTH MQTT_TOPIC_BASE "/status/device_health"
#define MQTT_TOPIC_ALERTS MQTT_TOPIC_BASE "/alerts/emergency"
// Sensor Objects
DHT dht(DHT_PIN, DHT_TYPE);
MPU6050 mpu6050(Wire);
OneWire oneWire(DS18B20_PIN);
DallasTemperature ds18b20(&oneWire);
// WiFi and MQTT Clients
WiFiClient espClient;
PubSubClient client(espClient);
// Genova Brignole Station, Italy
float gpsLatitude = 44.4034;
float gpsLongitude = 8.9720;
float gpsSpeed = 0.0; // Speed in km/h
char gpsStatus = 'A'; // GPS status (A = Active, V = Void)
bool gpsDataValid = true; // Always valid for simulation
unsigned long lastGPSUpdate = 0;
// Simplified GPS Linear Movement
struct GPSPoint {
float lat;
float lon;
float elevation;
};
// Define start and end points for linear movement
const GPSPoint startPoint = {44.4034, 8.9720, 50.0}; // Genova Brignole Station
const GPSPoint endPoint = {44.4084, 8.9770, 75.0}; // Spianata Castelletto viewpoint
// Movement state
float routeProgress = 0.0; // Progress from start to end (0.0 to 1.0)
bool movingForward = true; // Direction of movement
float totalDistance = 0.72; // Approximate distance in km between points
float currentElevation = 50.0;
// Health Thresholds
float minHeartRate = 60.0; // Minimum heart rate for alerts
float maxHeartRate = 180.0; // Maximum heart rate for alerts (changed to float)
float maxTemperature = 40.0; // °C
float maxBodyTemperature = 38.5; // °C for body temperature
float maxAcceleration = 2.5; // g-force
float minBreathingRate = 12.0; // breaths per minute (dynamic based on HR:BR ratio)
float maxBreathingRate = 60.0; // breaths per minute (dynamic based on HR:BR ratio)
// Data Filtering Variables
float heartRateBuffer[5] = {0};
float tempBuffer[5] = {0};
float bodyTempBuffer[5] = {0};
float breathingBuffer[10] = {0}; // Larger buffer for breathing signal analysis
int bufferIndex = 0;
int breathingBufferIndex = 0;
bool bufferFull = false;
bool breathingBufferFull = false;
// Breathing Rate Detection Variables
int lastBreathingValue = 0;
unsigned long lastBreathTime = 0;
float currentBreathingRate = 15.0; // Default breathing rate
// HRV Variables
float currentHRV = 60.0; // Default HRV value in milliseconds
// ECG Variables
float ecgPWaveAmplitude = 0.2; // P wave amplitude (0-0.5 mV)
float ecgRhythmRegularity = 1.0; // Rhythm regularity factor (0-2.0)
float ecgQRSAmplitude = 1.0; // QRS complex amplitude (0.5-2.0 mV)
float ecgQRSWidth = 0.08; // QRS width in seconds (0.06-0.12)
String ecgClassification = "Normal";
float ecgCurrentSample = 0.0; // Current ECG sample value
unsigned long lastECGUpdate = 0;
float ecgTimeIndex = 0.0; // Time index for ECG waveform generation
// Cycling Simulation Variables
float cyclingIntensity = 0.0; // 0.0 = rest, 1.0 = maximum effort
String currentExertionLevel = "Rest";
unsigned long lastIntensityChange = 0;
float intensityTarget = 0.0;
float breathingVariability = 0.0; // Add natural breathing variation
// Automatic Cycling Scenario Variables
int currentScenario = 0; // Current workout scenario index
unsigned long scenarioStartTime = 0; // When current scenario started
unsigned long scenarioElapsedTime = 0; // Time elapsed in current scenario
// Store scenario names in flash memory using PROGMEM
const char scenario0[] PROGMEM = "Morning Commute";
const char scenario1[] PROGMEM = "Interval Training";
const char scenario2[] PROGMEM = "Hill Climb";
const char scenario3[] PROGMEM = "Race Simulation";
const char scenario4[] PROGMEM = "Endurance Ride";
const char* const scenarioNames[] PROGMEM = {
scenario0, scenario1, scenario2, scenario3, scenario4
};
int scenarioDurations[] = {1200, 1800, 1500, 2100, 2400}; // Duration in seconds (20min, 30min, 25min, 35min, 40min)
bool automaticMode = true; // Enable automatic scenario cycling
// Helper function to read scenario names from PROGMEM
String getScenarioName(int index) {
char buffer[20];
strcpy_P(buffer, (char*)pgm_read_word(&(scenarioNames[index])));
return String(buffer);
}
// Dynamic Heart Rate Variables
float simulatedHeartRate = 70.0; // Current simulated heart rate
float heartRateTarget = 70.0; // Target heart rate based on intensity
float restingHeartRate = 65.0; // Individual's resting heart rate
float maxSimulatedHeartRate = 185.0; // Individual's maximum heart rate for simulation
unsigned long lastHeartRateUpdate = 0;
/*
* AUTOMATIC CYCLING SIMULATION:
* The system automatically cycles through realistic workout scenarios:
*
* 1. Morning Commute (20min): Easy start → steady → stops → final push
* 2. Interval Training (30min): Warm-up → intervals → recovery → cool-down
* 3. Hill Climb (25min): Flat → gradual climb → steep → descent
* 4. Race Simulation (35min): Warm-up → pace build → sprints → final sprint
* 5. Endurance Ride (40min): Long steady effort with gentle variations
*
* REALISTIC PHYSIOLOGICAL CHAIN:
* Cycling Intensity → Heart Rate → Breathing Rate
*
* Heart rate dynamically changes based on cycling effort (65-185 bpm range)
* Breathing rate uses dynamic HR:BR ratio based on exertion level:
* - Rest/Recovery: 4.0-5.0:1 ratio (slower breathing)
* - Light Activity: 3.5-4.1:1 ratio
* - Moderate Exercise: 3.0-3.6:1 ratio
* - Intense Exercise: 2.5-3.1:1 ratio
* - Maximum Effort: 2.0-2.7:1 ratio (faster breathing)
* Smooth transitions simulate real cardiovascular response times
*
* Use the potentiometer to enable/disable automatic mode or manual override.
*/
// Timing Variables
unsigned long lastSensorRead = 0;
unsigned long lastMQTTPublish = 0;
unsigned long lastHealthCheck = 0;
const unsigned long SENSOR_INTERVAL = 1000; // 1 second
const unsigned long MQTT_INTERVAL = 2000; // 2 seconds
const unsigned long HEALTH_INTERVAL = 30000; // 30 seconds
// Data validation - last known good values
float lastValidHeartRate = 70.0;
float lastValidTemperature = 25.0;
float lastValidBodyTemperature = 37.0;
float lastValidBreathingRate = 15.0;
float lastValidHRV = 60.0;
float lastValidAcceleration = 1.0;
// Data validation functions
float validateHeartRate(float hr) {
if (hr < 30 || hr > 220 || isnan(hr)) {
Serial.printf("Invalid HR detected: %.1f, using last valid: %.1f\n", hr, lastValidHeartRate);
return lastValidHeartRate;
}
lastValidHeartRate = hr;
return hr;
}
float validateTemperature(float temp) {
if (temp < -20 || temp > 60 || isnan(temp)) {
Serial.printf("Invalid temp detected: %.1f, using last valid: %.1f\n", temp, lastValidTemperature);
return lastValidTemperature;
}
lastValidTemperature = temp;
return temp;
}
float validateBodyTemperature(float bodyTemp) {
if (bodyTemp < 30 || bodyTemp > 45 || isnan(bodyTemp)) {
Serial.printf("Invalid body temp detected: %.1f, using last valid: %.1f\n", bodyTemp, lastValidBodyTemperature);
return lastValidBodyTemperature;
}
lastValidBodyTemperature = bodyTemp;
return bodyTemp;
}
float validateBreathingRate(float br) {
if (br < 5 || br > 100 || isnan(br)) {
Serial.printf("Invalid breathing rate detected: %.1f, using last valid: %.1f\n", br, lastValidBreathingRate);
return lastValidBreathingRate;
}
lastValidBreathingRate = br;
return br;
}
float validateHRV(float hrv) {
if (hrv < 10 || hrv > 150 || isnan(hrv)) {
Serial.printf("Invalid HRV detected: %.1f, using last valid: %.1f\n", hrv, lastValidHRV);
return lastValidHRV;
}
lastValidHRV = hrv;
return hrv;
}
float validateAcceleration(float accel) {
if (accel < 0 || accel > 10 || isnan(accel)) {
Serial.printf("Invalid acceleration detected: %.1f, using last valid: %.1f\n", accel, lastValidAcceleration);
return lastValidAcceleration;
}
lastValidAcceleration = accel;
return accel;
}
// Device Health Variables
float batteryLevel = 95.0 + random(0, 6); // Start with random 95-100%
unsigned long lastBatteryDrain = 0;
const unsigned long BATTERY_DRAIN_INTERVAL = 120000; // 2 minutes in milliseconds
bool wifiConnected = false;
bool mqttConnected = false;
bool sensorsWorking = true;
// WiFi connection state tracking
unsigned long lastWifiAttempt = 0;
int wifiAttempts = 0;
const unsigned long WIFI_RETRY_INTERVAL = 500;
const int MAX_WIFI_ATTEMPTS = 20;
// ADC reading tolerance constants
const int ADC_AUTO_MODE_THRESHOLD = 100; // Values below this are considered "auto mode"
// TFT display bounds constants
const int TFT_WIDTH = 320;
const int TFT_HEIGHT = 240;
// Structured Alert System
typedef enum {
INFO = 0,
WARNING = 1,
CRITICAL = 2,
EMERGENCY = 3
} AlertLevel;
// Function forward declarations
void publishAlert(const String& message, const String& severity = "high");
void triggerAlert(AlertLevel level, const String& message);
// Alert system variables
unsigned long lastAlertTime = 0;
const unsigned long ALERT_COOLDOWN = 10000; // 10 second cooldown between alerts
// Emergency-specific cooldown to prevent panic button spam
unsigned long lastEmergencyTime = 0;
const unsigned long EMERGENCY_COOLDOWN = 30000; // 30 second cooldown for emergency alerts
void triggerAlert(AlertLevel level, const String& message) {
unsigned long currentTime = millis();
// Special cooldown handling for emergency alerts
if (level == EMERGENCY) {
if (currentTime - lastEmergencyTime < EMERGENCY_COOLDOWN) {
Serial.println("🚫 Emergency alert ignored (cooldown active)");
return;
}
lastEmergencyTime = currentTime;
}
// Regular alert cooldown for non-emergency alerts
if (level < CRITICAL && currentTime - lastAlertTime < ALERT_COOLDOWN) {
return;
}
switch(level) {
case EMERGENCY:
// Send emergency alert (would integrate with SMS/emergency services)
Serial.println("🚨 EMERGENCY ALERT: " + message);
publishAlert(message, "EMERGENCY");
// Fall through to CRITICAL
case CRITICAL:
tone(BUZZER_PIN, 3000, 1000); // High pitch, 1 second
digitalWrite(RED_LED_PIN, HIGH);
publishAlert(message, "CRITICAL");
Serial.println("🔴 CRITICAL ALERT: " + message);
lastAlertTime = currentTime;
break;
case WARNING:
tone(BUZZER_PIN, 1500, 500); // Medium pitch, 0.5 seconds
digitalWrite(RED_LED_PIN, HIGH);
publishAlert(message, "WARNING");
Serial.println("🟡 WARNING: " + message);
lastAlertTime = currentTime;
break;
case INFO:
Serial.println("ℹ️ INFO: " + message);
break;
}
}
// Initialize GPS position
void initializeGPS() {
routeProgress = 0.0;
movingForward = true;
gpsLatitude = startPoint.lat;
gpsLongitude = startPoint.lon;
currentElevation = startPoint.elevation;
}
// Calculate elevation-based speed adjustment for linear route
float calculateElevationSpeedAdjustment() {
float elevationChange = endPoint.elevation - startPoint.elevation;
float totalElevationGain = abs(elevationChange);
// Calculate current grade based on direction
float currentGrade = 0.0;
if (movingForward) {
currentGrade = (elevationChange / (totalDistance * 1000.0)) * 100.0; // Convert to percentage
} else {
currentGrade = -(elevationChange / (totalDistance * 1000.0)) * 100.0; // Reverse direction
}
// Adjust speed based on grade
if (currentGrade > 3.0) { // Uphill
return 0.7; // 30% speed reduction
} else if (currentGrade < -3.0) { // Downhill
return 1.2; // 20% speed increase
}
return 1.0; // Flat terrain
}
// Simplified GPS Linear Movement Simulation
void simulateGPS() {
unsigned long currentTime = millis();
// Initialize GPS on first run
static bool gpsInitialized = false;
if (!gpsInitialized) {
initializeGPS();
gpsInitialized = true;
}
// Update GPS data every second
if (currentTime - lastGPSUpdate >= 1000) {
// Read speed potentiometer for speed control
int speedPotValue = analogRead(SPEED_PIN);
float baseSpeed = 0.0;
// Calculate base speed
if (speedPotValue <= ADC_AUTO_MODE_THRESHOLD) { // Automatic mode
if (automaticMode && cyclingIntensity > 0) {
baseSpeed = 20.0 + (cyclingIntensity * 20.0); // 20-40 km/h base range
} else {
baseSpeed = 25.0 + 10.0 * sin(currentTime / 15000.0); // 15-35 km/h varying
}
} else {
// Manual speed control
baseSpeed = max(0.0, (speedPotValue - ADC_AUTO_MODE_THRESHOLD) / (4095.0 - ADC_AUTO_MODE_THRESHOLD) * 50.0);
baseSpeed = constrain(baseSpeed, 0.0, 50.0);
}
// Apply elevation-based speed adjustment
float elevationAdjustment = calculateElevationSpeedAdjustment();
gpsSpeed = baseSpeed * elevationAdjustment;
// Add natural variation
float speedVariation = (random(-20, 21) / 10.0); // ±2 km/h variation
gpsSpeed += speedVariation;
gpsSpeed = constrain(gpsSpeed, 0.0, 60.0);
// Update position along linear route
if (gpsSpeed > 0) {
// Calculate progress increment based on speed
// Speed in km/h, time in seconds, distance in km
float progressIncrement = (gpsSpeed / 3600.0) / totalDistance; // Progress per second
if (movingForward) {
routeProgress += progressIncrement;
if (routeProgress >= 1.0) {
routeProgress = 1.0;
movingForward = false; // Reverse direction
}
} else {
routeProgress -= progressIncrement;
if (routeProgress <= 0.0) {
routeProgress = 0.0;
movingForward = true; // Forward direction
}
}
// Linear interpolation between start and end points
gpsLatitude = startPoint.lat + (endPoint.lat - startPoint.lat) * routeProgress;
gpsLongitude = startPoint.lon + (endPoint.lon - startPoint.lon) * routeProgress;
currentElevation = startPoint.elevation + (endPoint.elevation - startPoint.elevation) * routeProgress;
}
gpsStatus = 'A';
gpsDataValid = true;
lastGPSUpdate = currentTime;
}
}
// Function to read and convert pulse sensor to heart rate
float readPulseSensor(int16_t pulseValue) {
// The custom pulse sensor chip has two controls:
// - Mode: 0 = Manual (use Heart Rate slider), 1 = Simulated Cyclist
// - Heart Rate: 0-200 bpm slider value
// Convert ADC reading to heart rate
// The chip outputs the heart rate value directly through ADC
// Map ADC range (0-4095) to heart rate range (0-200)
float heartRate = (float)pulseValue * 200.0 / PULSE_SENSOR_ADC_MAX;
// Return the heart rate (0 means use simulation mode)
return heartRate;
}
// ECG Signal Generation Functions
float generateECGSample(float heartRate, float timeIndex) {
// Convert heart rate to RR interval (time between beats)
float rrInterval = 60.0 / heartRate; // seconds per beat
// Normalize time within one cardiac cycle (0 to 1)
float normalizedTime = fmod(timeIndex, rrInterval) / rrInterval;
// Add rhythm irregularity if ecgRhythmRegularity is not 1.0
if (ecgRhythmRegularity != 1.0) {
float irregularity = (ecgRhythmRegularity - 1.0) * 0.3 * sin(timeIndex * 3.14159 * 2.5);
normalizedTime += irregularity;
if (normalizedTime < 0) normalizedTime += 1.0;
if (normalizedTime > 1.0) normalizedTime -= 1.0;
}
float ecgSample = 0.0;
// P Wave (0.08 - 0.20 of cycle)
if (normalizedTime >= 0.08 && normalizedTime <= 0.20) {
float pTime = (normalizedTime - 0.08) / 0.12;
ecgSample += ecgPWaveAmplitude * exp(-pow((pTime - 0.5) * 4, 2));
}
// QRS Complex (0.35 - 0.45 of cycle)
if (normalizedTime >= 0.35 && normalizedTime <= 0.45) {
float qrsTime = (normalizedTime - 0.35) / 0.10;
// R wave - sharp peak
if (qrsTime >= 0.4 && qrsTime <= 0.6) {
ecgSample += ecgQRSAmplitude * exp(-pow((qrsTime - 0.5) * 8, 2));
}
// Q and S waves - small negative deflections
else if (qrsTime < 0.4) {
ecgSample -= ecgQRSAmplitude * 0.2 * exp(-pow((qrsTime - 0.2) * 6, 2));
}
else {
ecgSample -= ecgQRSAmplitude * 0.3 * exp(-pow((qrsTime - 0.8) * 6, 2));
}
}
// T Wave (0.55 - 0.75 of cycle)
if (normalizedTime >= 0.55 && normalizedTime <= 0.75) {
float tTime = (normalizedTime - 0.55) / 0.20;
ecgSample += (ecgQRSAmplitude * 0.3) * exp(-pow((tTime - 0.5) * 3, 2));
}
return ecgSample;
}
void readECGPotentiometers() {
// Read ECG control potentiometers
int pWavePot = analogRead(ECG_P_WAVE_PIN);
int rhythmPot = analogRead(ECG_RHYTHM_PIN);
int qrsPot = analogRead(ECG_QRS_PIN);
// Handle disconnected potentiometers (reading near 0 or 4095) by using default values
if (pWavePot < 50) pWavePot = 2048; // Use middle value if disconnected low
if (rhythmPot < 50) rhythmPot = 2048; // Use middle value if disconnected low
if (qrsPot < 50) qrsPot = 2048; // Use middle value if disconnected low
// IMPROVED ECG PARAMETER MAPPING - designed for reliable event triggering
// P-Wave Amplitude: Redesigned to easily trigger AFib at low values
// Range: 0.02-0.6mV with AFib trigger zone at 0-25% of pot range (<0.1mV)
if (pWavePot <= 1024) { // Bottom 25% of pot range (0-1024)
ecgPWaveAmplitude = map(pWavePot, 0, 1024, 20, 90) / 1000.0; // 0.02-0.09mV (AFib zone)
} else {
ecgPWaveAmplitude = map(pWavePot, 1025, 4095, 100, 600) / 1000.0; // 0.1-0.6mV (normal zone)
}
// Rhythm Regularity: Redesigned to easily trigger AFib at low values
// Range: 0.3-2.0 with AFib trigger zone at 0-35% of pot range (<0.7)
if (rhythmPot <= 1434) { // Bottom 35% of pot range (0-1434)
ecgRhythmRegularity = map(rhythmPot, 0, 1434, 30, 65) / 100.0; // 0.3-0.65 (AFib zone)
} else {
ecgRhythmRegularity = map(rhythmPot, 1435, 4095, 70, 200) / 100.0; // 0.7-2.0 (normal zone)
}
// QRS Amplitude: Redesigned to trigger Noisy at both extremes
// Range: 0.4-2.2mV with Noisy zones at 0-20% (<0.6mV) and 80-100% (>1.9mV)
if (qrsPot <= 819) { // Bottom 20% of pot range (0-819)
ecgQRSAmplitude = map(qrsPot, 0, 819, 400, 590) / 1000.0; // 0.4-0.59mV (Noisy zone)
} else if (qrsPot >= 3276) { // Top 20% of pot range (3276-4095)
ecgQRSAmplitude = map(qrsPot, 3276, 4095, 1910, 2200) / 1000.0; // 1.91-2.2mV (Noisy zone)
} else { // Middle 60% of pot range (820-3275)
ecgQRSAmplitude = map(qrsPot, 820, 3275, 600, 1900) / 1000.0; // 0.6-1.9mV (normal zone)
}
// Adjust QRS width based on amplitude (extreme values = wider, noisier)
if (ecgQRSAmplitude < 0.6 || ecgQRSAmplitude > 1.9) {
ecgQRSWidth = 0.15; // Wide, noisy QRS
} else {
ecgQRSWidth = 0.08; // Normal QRS width
}
}
String classifyECG(float heartRate) {
// Read current potentiometer positions
readECGPotentiometers();
// Check for noisy signal first (extreme potentiometer positions)
if (ecgQRSAmplitude < 0.6 || ecgQRSAmplitude > 1.9 ||
ecgPWaveAmplitude > 0.45 || ecgRhythmRegularity > 1.8) {
return "Noisy";
}
// Check for Atrial Fibrillation (irregular rhythm + absent P waves)
if (ecgRhythmRegularity < 0.7 && ecgPWaveAmplitude < 0.1) {
return "AFib";
}
// Check for Bradycardia (HR < 60 BPM but otherwise normal)
if (heartRate < 60.0 && ecgRhythmRegularity >= 0.7 && ecgPWaveAmplitude >= 0.1) {
return "Brady";
}
// Normal Rhythm - FIXED: More inclusive ranges
if (heartRate >= 60.0 && heartRate <= 100.0 &&
ecgRhythmRegularity >= 0.7 && ecgPWaveAmplitude >= 0.1 &&
ecgQRSAmplitude >= 0.6 && ecgQRSAmplitude <= 1.9) {
return "Normal";
}
// Tachycardia but normal morphology - FIXED: More inclusive
if (heartRate > 100.0 &&
ecgRhythmRegularity >= 0.7 && ecgPWaveAmplitude >= 0.1 &&
ecgQRSAmplitude >= 0.6 && ecgQRSAmplitude <= 1.9) {
return "Normal"; // Tachycardia but normal morphology
}
return "Unknown";
}
void updateECGSignal() {
unsigned long currentTime = millis();
if (currentTime - lastECGUpdate >= 10) { // Update every 10ms (100Hz sampling)
// Use the existing system heart rate
float currentHR = simulatedHeartRate;
// Generate ECG sample using current heart rate
ecgCurrentSample = generateECGSample(currentHR, ecgTimeIndex);
// Classify ECG based on current heart rate and morphology
ecgClassification = classifyECG(currentHR);
// Increment time index
ecgTimeIndex += 0.01; // 10ms increment
lastECGUpdate = currentTime;
}
}
void checkEmergencyButton() {
unsigned long currentTime = millis();
bool buttonState = digitalRead(EMERGENCY_BUTTON_PIN);
// Check if button state has changed and debounce
if (buttonState != lastButtonState && (currentTime - lastDebounceTime) > DEBOUNCE_DELAY) {
lastDebounceTime = currentTime;
lastButtonState = buttonState;
if (buttonState == LOW && !emergencyAlertSent) { // Button pressed AND no alert sent yet
// Immediate feedback for button press
tone(BUZZER_PIN, 2000, 200); // Short confirmation beep: 2kHz for 200ms
emergencyButtonPressed = true; // Set UI feedback flag
emergencyButtonPressTime = currentTime; // Record when pressed
// Mark emergency alert as sent to prevent repeats
emergencyAlertSent = true;
emergencyAlertTime = currentTime;
// Trigger emergency alert
triggerAlert(EMERGENCY, "USER EMERGENCY - Manual panic button pressed");
Serial.println("🚨 EMERGENCY BUTTON PRESSED - ALERT SENT!");
// Add user emergency alert to MQTT
if (client.connected()) {
StaticJsonDocument<512> doc;
doc["timestamp"] = millis();
doc["device_id"] = "ESP32_001";
doc["alert_type"] = "user_emergency";
doc["message"] = "Manual panic button activated by user";
doc["severity"] = "EMERGENCY";
doc["location"]["latitude"] = gpsLatitude;
doc["location"]["longitude"] = gpsLongitude;
String emergencyJson;
serializeJson(doc, emergencyJson);
client.publish(MQTT_TOPIC_ALERTS, emergencyJson.c_str());
}
} else if (buttonState == HIGH && emergencyAlertSent) {
// Button released - reset emergency state for next press
emergencyAlertSent = false;
Serial.println("🔓 Emergency button released - Ready for next emergency");
}
}
// Also reset emergency state after 60 seconds (safety timeout)
if (emergencyAlertSent && (currentTime - emergencyAlertTime > 60000)) {
emergencyAlertSent = false;
Serial.println("⏰ Emergency state reset (60s timeout)");
}
}
void updateEmergencyFeedback() {
// Clear emergency button feedback after timeout
if (emergencyButtonPressed && (millis() - emergencyButtonPressTime > EMERGENCY_FEEDBACK_DURATION)) {
emergencyButtonPressed = false;
}
}
void checkPowerSwitch() {
unsigned long currentTime = millis();
bool switchState = digitalRead(POWER_SWITCH_PIN);
if (switchState != lastPowerSwitchState && (currentTime - lastPowerDebounceTime) > DEBOUNCE_DELAY) {
lastPowerDebounceTime = currentTime;
lastPowerSwitchState = switchState;
if (switchState == LOW) { // Switch pressed (turned off)
// Show turning off message if display is enabled
if (displayEnabled) {
tft.fillScreen(ILI9341_BLACK);
tft.setTextColor(ILI9341_RED);
tft.setTextSize(2);
tft.setCursor(50, 100);
tft.println("Turning off device...");
delay(1000); // Show message for 1 second
tft.fillScreen(ILI9341_BLACK); // Clear screen completely
}
devicePoweredOn = false;
Serial.println("Device powered OFF");
} else { // Switch released (turned on)
devicePoweredOn = true;
Serial.println("Device powered ON");
// Show startup message if display is enabled
if (displayEnabled) {
tft.fillScreen(ILI9341_BLACK);
tft.setTextColor(ILI9341_GREEN);
tft.setTextSize(2);
tft.setCursor(50, 100);
tft.println("Device starting up...");
delay(1000); // Show message for 1 second
}
}
}
}
void checkDisplayButton() {
unsigned long currentTime = millis();
bool buttonState = digitalRead(DISPLAY_BUTTON_PIN);
if (buttonState != lastDisplayButtonState && (currentTime - lastDisplayDebounceTime) > DEBOUNCE_DELAY) {
lastDisplayDebounceTime = currentTime;
lastDisplayButtonState = buttonState;
if (buttonState == LOW) { // Button pressed
displayEnabled = !displayEnabled; // Toggle display state
Serial.println(displayEnabled ? "Display ON" : "Display OFF");
if (!displayEnabled) {
showDisplayOffScreen(); // Show display off indicators
}
}
}
}
void setup() {
Serial.begin(115200);
// Initialize validation variables with safe defaults
lastValidHeartRate = 70.0;
lastValidTemperature = 25.0;
lastValidBodyTemperature = 37.0;
lastValidBreathingRate = 15.0;
lastValidHRV = 60.0;
lastValidAcceleration = 1.0;
// Initialize sensor buffers with safe default values
for (int i = 0; i < 5; i++) {
heartRateBuffer[i] = lastValidHeartRate;
tempBuffer[i] = lastValidTemperature;
bodyTempBuffer[i] = lastValidBodyTemperature;
}
for (int i = 0; i < 10; i++) {
breathingBuffer[i] = lastValidBreathingRate;
}
// Initialize ECG parameters with normal values
ecgPWaveAmplitude = 0.2; // Normal P wave amplitude
ecgRhythmRegularity = 1.0; // Normal rhythm regularity
ecgQRSAmplitude = 1.0; // Normal QRS amplitude
ecgQRSWidth = 0.08; // Normal QRS width
ecgClassification = "Normal";
// Initialize TFT Display
tft.begin();
tft.setRotation(1); // Landscape orientation
tft.fillScreen(ILI9341_BLACK);
tft.setTextColor(ILI9341_WHITE);
tft.setTextSize(2);
tft.setCursor(0, 0);
tft.println("IoT Wearables");
tft.setCursor(0, 20);
tft.println("Initializing...");
// Initialize I2C for MPU6050
Wire.begin(21, 22); // SDA, SCL for ESP32
// Initialize LED pins and buttons
pinMode(RED_LED_PIN, OUTPUT);
pinMode(GREEN_LED_PIN, OUTPUT);
pinMode(BUZZER_PIN, OUTPUT);
pinMode(EMERGENCY_BUTTON_PIN, INPUT_PULLUP); // Use internal pull-up resistor
pinMode(POWER_SWITCH_PIN, INPUT_PULLUP); // Power switch with pull-up
pinMode(DISPLAY_BUTTON_PIN, INPUT_PULLUP); // Display button with pull-up
digitalWrite(BUZZER_PIN, LOW);
// Initialize sensors
dht.begin();
ds18b20.begin();
mpu6050.begin();
mpu6050.calcGyroOffsets(true);
// GPS simulation - no hardware initialization needed
Serial.println("GPS simulation initialized");
// Display startup message
tft.fillScreen(ILI9341_BLACK);
tft.setCursor(0, 0);
tft.println("IoT Wearables");
tft.setCursor(0, 20);
tft.println("System Ready");
// Connect to WiFi
connectToWiFi();
// Setup MQTT
client.setServer(MQTT_SERVER, MQTT_PORT);
client.setBufferSize(4096); // Increase buffer size for large sensor data messages
Serial.println("Setup completed successfully!");
tft.fillScreen(ILI9341_BLACK);
tft.setCursor(0, 0);
tft.println("System Ready");
}
void loop() {
unsigned long currentTime = millis();
// Check WiFi connection non-blocking
checkWiFiConnection();
// Ensure MQTT connection
if (!client.connected()) {
reconnectMQTT();
}
client.loop();
// Simulate GPS data
simulateGPS();
// Update ECG signal
updateECGSignal();
// Check all buttons
checkPowerSwitch();
checkDisplayButton();
checkEmergencyButton();
// Update emergency feedback timing
updateEmergencyFeedback();
// Read sensors at specified interval
if (currentTime - lastSensorRead >= SENSOR_INTERVAL) {
readAndProcessSensors();
lastSensorRead = currentTime;
}
// Publish data via MQTT at specified interval
if (currentTime - lastMQTTPublish >= MQTT_INTERVAL) {
publishSensorData();
lastMQTTPublish = currentTime;
}
// Perform health check at specified interval
if (currentTime - lastHealthCheck >= HEALTH_INTERVAL) {
performHealthCheck();
lastHealthCheck = currentTime;
}
// Update display (only if device is powered on)
if (devicePoweredOn) {
if (displayEnabled) {
updateDisplay();
} else {
updateDisplayOffBreathing(); // Show breathing effect when display is off
}
}
}
void showDisplayOffScreen() {
tft.fillScreen(ILI9341_BLACK);
// Show power ON indicator in top left
tft.setTextSize(1);
tft.setTextColor(ILI9341_GREEN);
tft.setCursor(5, 5);
tft.print("PWR ON");
// Show time in top right
tft.setCursor(260, 5);
tft.printf("%.0fs", millis()/1000.0);
// Initial display off screen setup
breathingPhase = 0.0;
updateDisplayOffBreathing();
}
void updateDisplayOffBreathing() {
unsigned long currentTime = millis();
// Update breathing animation
if (currentTime - lastBreathingUpdate >= BREATHING_INTERVAL) {
lastBreathingUpdate = currentTime;
breathingPhase += BREATHING_SPEED;
if (breathingPhase > 2 * PI) {
breathingPhase = 0.0;
}
// Calculate breathing brightness (30% to 60%)
float breathingIntensity = 0.3 + 0.3 * (sin(breathingPhase) + 1) / 2;
uint16_t breathingColor = getBreathingColor(breathingIntensity);
// Clear center area and redraw with new brightness
tft.fillRect(80, 60, 160, 120, ILI9341_BLACK);
// Draw sleep icon (zzZ)
tft.setTextSize(4);
tft.setTextColor(breathingColor);
tft.setCursor(120, 80);
tft.print("zzZ");
// Draw eye with line through it (simplified as text)
tft.setTextSize(2);
tft.setCursor(130, 120);
tft.print("[-]");
// Draw instruction text
tft.setTextSize(1);
tft.setTextColor(breathingColor);
tft.setCursor(90, 150);
tft.print("Display OFF");
tft.setCursor(80, 165);
tft.print("Press green to wake");
// Update status indicators (don't use breathing effect for these)
tft.setTextSize(1);
tft.setTextColor(ILI9341_GREEN);
tft.setCursor(5, 5);
tft.print("PWR ON");
tft.setCursor(260, 5);
tft.printf("%.0fs", millis()/1000.0);
}
}
uint16_t getBreathingColor(float intensity) {
// Convert intensity (0.0-1.0) to RGB565 color
// Use white color with varying intensity
uint8_t brightness = (uint8_t)(intensity * 255);
uint8_t r = brightness >> 3; // 5 bits for red
uint8_t g = brightness >> 2; // 6 bits for green
uint8_t b = brightness >> 3; // 5 bits for blue
return (r << 11) | (g << 5) | b;
}
void readAndProcessSensors() {
// Read pulse sensor and convert to heart rate
int16_t pulseValue = analogRead(PULSE_PIN);
float pulseHeartRate = readPulseSensor(pulseValue);
// Use pulse sensor's built-in mode control:
// - If Heart Rate slider is 0: use automatic scenarios
// - If Heart Rate slider > 0: use the manual heart rate value
if (pulseHeartRate > 0) {
// Manual mode: use heart rate from pulse sensor chip slider
simulatedHeartRate = pulseHeartRate;
Serial.printf("Heart Rate: %.1f bpm (MANUAL - Pulse Sensor Slider, ADC: %d)\n", pulseHeartRate, pulseValue);
automaticMode = false; // Disable automatic scenarios
} else {
// Simulation mode: use automatic cycling scenarios
calculateDynamicHeartRate();
Serial.printf("Heart Rate: %.1f bpm (SIMULATION - Auto Scenarios)\n", simulatedHeartRate);
automaticMode = true; // Enable automatic scenarios
}
// Calculate breathing rate based on heart rate
calculateDynamicBreathingRate(simulatedHeartRate);
// Calculate HRV based on heart rate
calculateDynamicHRV(simulatedHeartRate);
// Read Temperature and Humidity (ambient)
float temperature = dht.readTemperature();
float humidity = dht.readHumidity();
// Read Body Temperature (DS18B20)
ds18b20.requestTemperatures();
float bodyTemperature = ds18b20.getTempCByIndex(0);
// Read Accelerometer
mpu6050.update();
float accelX = mpu6050.getAccX();
float accelY = mpu6050.getAccY();
float accelZ = mpu6050.getAccZ();
float totalAccel = sqrt(accelX*accelX + accelY*accelY + accelZ*accelZ);
// Validate sensor readings before buffering
float validatedHeartRate = validateHeartRate(simulatedHeartRate);
float validatedTemperature = validateTemperature(temperature);
float validatedBodyTemperature = validateBodyTemperature(bodyTemperature);
float validatedBreathingRate = validateBreathingRate(currentBreathingRate);
float validatedHRV = validateHRV(currentHRV);
float validatedAcceleration = validateAcceleration(totalAccel);
// Apply filtering to validated sensor data
heartRateBuffer[bufferIndex] = validatedHeartRate;
tempBuffer[bufferIndex] = validatedTemperature;
bodyTempBuffer[bufferIndex] = validatedBodyTemperature;
bufferIndex = (bufferIndex + 1) % 5;
if (bufferIndex == 0) bufferFull = true;
// Calculate filtered values
float filteredHeartRate = calculateAverage(heartRateBuffer, bufferFull ? 5 : bufferIndex + 1);
float filteredTemperature = calculateAverage(tempBuffer, bufferFull ? 5 : bufferIndex + 1);
float filteredBodyTemperature = calculateAverage(bodyTempBuffer, bufferFull ? 5 : bufferIndex + 1);
// Check for alerts using validated data
checkAlerts(filteredHeartRate, filteredTemperature, filteredBodyTemperature, validatedAcceleration, validatedBreathingRate);
// Update sensor status
sensorsWorking = !isnan(temperature) && !isnan(humidity) && !isnan(bodyTemperature) && (pulseValue > 0);
// Print sensor data to Serial
Serial.println("=== Sensor Readings ===");
Serial.printf("Heart Rate: %.1f bpm (simulated: %.1f)\n", filteredHeartRate, simulatedHeartRate);
// Check if breathing rate is in manual or automatic mode
int breathingPotValue = analogRead(BREATHING_PIN);
if (breathingPotValue <= ADC_AUTO_MODE_THRESHOLD) {
// Automatic mode - show HR:BR ratio details
float currentRatio = calculateHRtoBreathingRatio(cyclingIntensity, currentExertionLevel);
Serial.printf("Breathing Rate: %.1f breaths/min (AUTO - Dynamic Ratio: %.1f:1, HR/%.1f=%.1f, Exertion: %s)\n",
currentBreathingRate, currentRatio, currentRatio, simulatedHeartRate/currentRatio, currentExertionLevel.c_str());
} else {
// Manual mode - show potentiometer control
Serial.printf("Breathing Rate: %.1f breaths/min (MANUAL - Pot: %d, Range: 1-60)\n",
currentBreathingRate, breathingPotValue);
}
// Show HRV information
int hrvPotValue = analogRead(HRV_PIN);
if (hrvPotValue <= ADC_AUTO_MODE_THRESHOLD) {
Serial.printf("HRV: %.1f ms (AUTO - Formula: 3600/HR, Pot: %d)\n", currentHRV, hrvPotValue);
} else {
Serial.printf("HRV: %.1f ms (MANUAL - Pot: %d, Range: 20-120)\n", currentHRV, hrvPotValue);
}
if (automaticMode) {
Serial.printf("Scenario: %s (%.1f%% complete, %02d:%02d remaining)\n",
getScenarioName(currentScenario).c_str(),
(float)scenarioElapsedTime / scenarioDurations[currentScenario] * 100,
(scenarioDurations[currentScenario] - scenarioElapsedTime) / 60,
(scenarioDurations[currentScenario] - scenarioElapsedTime) % 60);
}
Serial.printf("Ambient Temp: %.1f°C, Humidity: %.1f%%\n", filteredTemperature, humidity);
Serial.printf("Body Temperature: %.1f°C (raw: %.1f)\n", filteredBodyTemperature, bodyTemperature);
Serial.printf("Acceleration: X=%.2f, Y=%.2f, Z=%.2f, Total=%.2f g\n",
accelX, accelY, accelZ, totalAccel);
// GPS and Speed Information
int speedPotValue = analogRead(SPEED_PIN);
if (gpsDataValid) {
if (speedPotValue <= ADC_AUTO_MODE_THRESHOLD) {
Serial.printf("GPS: Lat=%.6f°, Lon=%.6f°, Speed=%.1f km/h, Elev=%.1fm (AUTO - Waypoint %d/%.0f%% - Pot: %d)\n",
gpsLatitude, gpsLongitude, gpsSpeed, currentElevation, (int)(routeProgress*100), movingForward ? 1 : 0, speedPotValue);
} else {
Serial.printf("GPS: Lat=%.6f°, Lon=%.6f°, Speed=%.1f km/h, Elev=%.1fm (MANUAL - Waypoint %d/%.0f%% - Pot: %d)\n",
gpsLatitude, gpsLongitude, gpsSpeed, currentElevation, (int)(routeProgress*100), movingForward ? 1 : 0, speedPotValue);
}
} else {
Serial.printf("GPS: Status=%c, Speed=%.1f km/h (SEARCHING...)\n",
gpsStatus, gpsSpeed);
}
// ECG Information
int pWavePot = analogRead(ECG_P_WAVE_PIN);
int rhythmPot = analogRead(ECG_RHYTHM_PIN);
int qrsPot = analogRead(ECG_QRS_PIN);
Serial.printf("ECG: Classification=%s, Sample=%.3fmV\n",
ecgClassification.c_str(), ecgCurrentSample);
Serial.println("--- IMPROVED ECG POTENTIOMETER CONTROLS (Bottom 3 pots in Wokwi) ---");
Serial.printf("POT 1 (GPIO 37) - P-Wave Amplitude: %d -> %.2fmV", pWavePot, ecgPWaveAmplitude);
if (pWavePot <= 1024) {
Serial.println(" [AFib TRIGGER ZONE - Turn LEFT for AFib]");
} else {
Serial.println(" [Normal Zone]");
}
Serial.printf("POT 2 (GPIO 38) - Rhythm Regularity: %d -> %.2f", rhythmPot, ecgRhythmRegularity);
if (rhythmPot <= 1434) {
Serial.println(" [AFib TRIGGER ZONE - Turn LEFT for AFib]");
} else {
Serial.println(" [Normal Zone]");
}
Serial.printf("POT 3 (GPIO 36) - QRS Amplitude: %d -> %.2fmV", qrsPot, ecgQRSAmplitude);
if (qrsPot <= 819) {
Serial.println(" [Noisy TRIGGER ZONE - Turn LEFT for Noisy]");
} else if (qrsPot >= 3276) {
Serial.println(" [Noisy TRIGGER ZONE - Turn RIGHT for Noisy]");
} else {
Serial.println(" [Normal Zone]");
}
Serial.println("TRIGGER INSTRUCTIONS:");
Serial.println("• AFib: Turn POT 1 (P-Wave) AND POT 2 (Rhythm) to LEFT (25% and 35% respectively)");
Serial.println("• Noisy: Turn POT 3 (QRS) to FAR LEFT (20%) OR FAR RIGHT (80%+)");
Serial.println("• Brady: Keep ECG pots normal + ensure Heart Rate < 60 bpm");
Serial.println();
}
void calculateDynamicHeartRate() {
unsigned long currentTime = millis();
// Update heart rate target based on cycling intensity
if (currentTime - lastHeartRateUpdate > 100) { // Update every 100ms
// Calculate target heart rate based on cycling intensity
// Intensity 0.0 = resting HR, Intensity 1.0 = max HR
heartRateTarget = restingHeartRate + (cyclingIntensity * (maxSimulatedHeartRate - restingHeartRate));
// Add some natural variation (±3 bpm)
float heartRateVariation = (random(-30, 31) / 10.0); // -3.0 to +3.0
heartRateTarget += heartRateVariation;
// Clamp to reasonable range
heartRateTarget = constrain(heartRateTarget, restingHeartRate - 5, maxSimulatedHeartRate + 5);
// Smooth heart rate changes (cardiovascular response time)
float hrStep = 0.8; // Heart rate change rate (bpm per update)
if (simulatedHeartRate < heartRateTarget) {
simulatedHeartRate += hrStep;
if (simulatedHeartRate > heartRateTarget) simulatedHeartRate = heartRateTarget;
} else if (simulatedHeartRate > heartRateTarget) {
simulatedHeartRate -= hrStep;
if (simulatedHeartRate < heartRateTarget) simulatedHeartRate = heartRateTarget;
}
lastHeartRateUpdate = currentTime;
}
}
float calculateHRtoBreathingRatio(float intensity, String exertionLevel) {
// Dynamic HR:BR ratio based on exertion level and physiological research
// Rest/Sleep: 4:1 to 5:1 (higher ratio)
// Light activity: 3.5:1 to 4:1
// Moderate exercise: 3:1 to 3.5:1
// Intense exercise: 2.5:1 to 3:1
// Maximum effort: 2:1 to 2.5:1 (lower ratio)
if (intensity < 0.1) {
// Rest/Recovery - higher ratio (slower breathing relative to HR)
return 4.5 + (random(-5, 6) / 10.0); // 4.0 to 5.0
} else if (intensity < 0.3) {
// Light activity
return 3.8 + (random(-3, 4) / 10.0); // 3.5 to 4.1
} else if (intensity < 0.6) {
// Moderate exercise
return 3.2 + (random(-2, 4) / 10.0); // 3.0 to 3.6
} else if (intensity < 0.8) {
// Intense exercise
return 2.8 + (random(-3, 3) / 10.0); // 2.5 to 3.1
} else {
// Maximum effort - lower ratio (faster breathing relative to HR)
return 2.3 + (random(-3, 4) / 10.0); // 2.0 to 2.7
}
}
void calculateDynamicBreathingRate(float heartRate) {
unsigned long currentTime = millis();
// Read potentiometer for breathing rate control
int breathingPotValue = analogRead(BREATHING_PIN);
// Check if potentiometer is at 0 (automatic HR-based breathing) or manual control
if (breathingPotValue <= ADC_AUTO_MODE_THRESHOLD) { // Near 0, use automatic HR-based breathing
// Mode control is now handled in readAndProcessSensors()
// This section focuses on automatic breathing rate calculation based on HR
// Generate automatic cycling scenarios
if (automaticMode) {
generateAutomaticScenario(currentTime);
}
// Smooth intensity changes (gradual transitions)
if (currentTime - lastIntensityChange > 100) { // Update every 100ms
float intensityStep = 0.02; // Gradual change rate
if (cyclingIntensity < intensityTarget) {
cyclingIntensity += intensityStep;
if (cyclingIntensity > intensityTarget) cyclingIntensity = intensityTarget;
} else if (cyclingIntensity > intensityTarget) {
cyclingIntensity -= intensityStep;
if (cyclingIntensity < intensityTarget) cyclingIntensity = intensityTarget;
}
lastIntensityChange = currentTime;
}
// Determine exertion level based on intensity
if (cyclingIntensity < 0.2) {
currentExertionLevel = "Rest";
} else if (cyclingIntensity < 0.4) {
currentExertionLevel = "Light";
} else if (cyclingIntensity < 0.6) {
currentExertionLevel = "Moderate";
} else if (cyclingIntensity < 0.8) {
currentExertionLevel = "Intense";
} else {
currentExertionLevel = "Maximum";
}
// Calculate breathing rate using dynamic HR:BR ratio based on exertion level
// Rest: HR:BR ≈ 4:1, Light exercise: 3.5:1, Intense: 3:1, Maximum: 2.5:1
float hrBrRatio = calculateHRtoBreathingRatio(cyclingIntensity, currentExertionLevel);
float baseBreathingRate = heartRate / hrBrRatio;
// Add natural breathing variability (±1 breath/min for more realistic variation)
breathingVariability += (random(-10, 11) / 10.0); // -1.0 to +1.0
breathingVariability = constrain(breathingVariability, -1.5, 1.5);
// Apply variability and smooth the result
currentBreathingRate = baseBreathingRate + breathingVariability;
// Clamp to physiologically reasonable range (dynamic based on new ratios)
currentBreathingRate = constrain(currentBreathingRate, 10, 80);
Serial.printf("Breathing Rate: %.1f breaths/min (AUTO - varies with HR, Pot: %d)\n", currentBreathingRate, breathingPotValue);
} else {
// Manual breathing rate control via potentiometer
// Map potentiometer value to breathing rate (1-60 breaths/min)
currentBreathingRate = map(breathingPotValue, ADC_AUTO_MODE_THRESHOLD, 4095, 1, 60);
// Set exertion level to manual for display purposes
currentExertionLevel = "Manual";
Serial.printf("Breathing Rate: %.1f breaths/min (MANUAL - Potentiometer, Pot: %d)\n", currentBreathingRate, breathingPotValue);
}
}
void calculateDynamicHRV(float heartRate) {
// Read potentiometer for HRV control
int hrvPotValue = analogRead(HRV_PIN);
// Check if potentiometer is at 0 (automatic HR-based HRV) or manual control
if (hrvPotValue <= ADC_AUTO_MODE_THRESHOLD) { // Near 0, use automatic HR-based HRV calculation
// Use formula: HRV = max(20, min(120, 3600/HR)) + random noise
float baseHRV = 3600.0f / heartRate; // Inverse relationship
baseHRV = max(20.0f, min(120.0f, baseHRV)); // Clamp to physiological range
// Add natural HRV variability (±5 ms for realism)
float hrvVariation = (random(-50, 51) / 10.0f); // -5.0 to +5.0 ms
currentHRV = baseHRV + hrvVariation;
// Final clamp to ensure we stay in range
currentHRV = constrain(currentHRV, 15.0f, 125.0f);
Serial.printf("HRV: %.1f ms (AUTO - Formula: 3600/%.1f=%.1f, +noise, Pot: %d)\n",
currentHRV, heartRate, baseHRV, hrvPotValue);
} else {
// Manual HRV control via potentiometer
// Map potentiometer value to HRV range (20-120 ms)
currentHRV = map(hrvPotValue, ADC_AUTO_MODE_THRESHOLD, 4095, 20, 120);
Serial.printf("HRV: %.1f ms (MANUAL - Potentiometer, Pot: %d, Range: 20-120)\n",
currentHRV, hrvPotValue);
}
}
void generateAutomaticScenario(unsigned long currentTime) {
// Initialize scenario timing
if (scenarioStartTime == 0) {
scenarioStartTime = currentTime;
}
// Calculate elapsed time in current scenario
scenarioElapsedTime = (currentTime - scenarioStartTime) / 1000; // Convert to seconds
// Check if current scenario is complete
if (scenarioElapsedTime >= scenarioDurations[currentScenario]) {
// Move to next scenario
currentScenario = (currentScenario + 1) % 5; // Cycle through 5 scenarios
scenarioStartTime = currentTime;
scenarioElapsedTime = 0;
}
// Calculate intensity based on current scenario and time
float progress = (float)scenarioElapsedTime / scenarioDurations[currentScenario];
switch (currentScenario) {
case 0: // Morning Commute
intensityTarget = calculateCommuteIntensity(progress);
break;
case 1: // Interval Training
intensityTarget = calculateIntervalIntensity(progress);
break;
case 2: // Hill Climb
intensityTarget = calculateHillClimbIntensity(progress);
break;
case 3: // Race Simulation
intensityTarget = calculateRaceIntensity(progress);
break;
case 4: // Endurance Ride
intensityTarget = calculateEnduranceIntensity(progress);
break;
}
}
float calculateCommuteIntensity(float progress) {
// Morning Commute: Easy start → steady → traffic stops → final push
if (progress < 0.1) return 0.1 + progress * 2.0; // Easy start
else if (progress < 0.7) return 0.3 + sin(progress * 15) * 0.1; // Steady with traffic variations
else if (progress < 0.85) return 0.2; // Traffic stop/slow
else return 0.4 + (progress - 0.85) * 2.0; // Final push
}
float calculateIntervalIntensity(float progress) {
// Interval Training: Warm-up → high intensity intervals → recovery → cool-down
if (progress < 0.15) return progress * 2.0; // Warm-up
else if (progress < 0.8) {
// Intervals: 30s high, 60s recovery pattern
float intervalTime = fmod((progress - 0.15) * scenarioDurations[1], 90);
return intervalTime < 30 ? 0.8 : 0.3; // High intensity or recovery
}
else return 0.3 - (progress - 0.8) * 1.5; // Cool-down
}
float calculateHillClimbIntensity(float progress) {
// Hill Climb: Flat → gradual climb → steep → descent
if (progress < 0.2) return 0.3; // Flat start
else if (progress < 0.6) return 0.3 + (progress - 0.2) * 1.25; // Gradual climb
else if (progress < 0.8) return 0.8; // Steep section
else return 0.8 - (progress - 0.8) * 2.5; // Descent
}
float calculateRaceIntensity(float progress) {
// Race Simulation: Warm-up → pace building → sprint intervals → final sprint
if (progress < 0.1) return progress * 3.0; // Warm-up
else if (progress < 0.6) return 0.3 + (progress - 0.1) * 1.0; // Pace building
else if (progress < 0.9) {
// Sprint intervals
float sprintCycle = fmod((progress - 0.6) * scenarioDurations[3], 120);
return sprintCycle < 20 ? 0.9 : 0.5; // 20s sprint, 100s recovery
}
else return 0.5 + (progress - 0.9) * 5.0; // Final sprint
}
float calculateEnduranceIntensity(float progress) {
// Endurance Ride: Long steady effort with gentle variations
float baseIntensity = 0.45;
float variation = sin(progress * 12) * 0.15; // Gentle rolling variations
return baseIntensity + variation;
}
float calculateAverage(float* buffer, int size) {
float sum = 0;
for (int i = 0; i < size; i++) {
sum += buffer[i];
}
return sum / size;
}
void checkAlerts(float heartRate, float temperature, float bodyTemperature, float acceleration, float breathingRate) {
bool anyAlert = false;
// Heart Rate Alerts - Medical thresholds
if (heartRate < 50) { // Severe bradycardia
triggerAlert(CRITICAL, "Severe Bradycardia: HR " + String((int)heartRate) + " bpm");
anyAlert = true;
} else if (heartRate > 200) { // Severe tachycardia
triggerAlert(CRITICAL, "Severe Tachycardia: HR " + String((int)heartRate) + " bpm");
anyAlert = true;
} else if (heartRate < minHeartRate || heartRate > maxHeartRate) { // Mild abnormal
triggerAlert(WARNING, "Abnormal Heart Rate: " + String((int)heartRate) + " bpm");
anyAlert = true;
}
// Body Temperature Alerts - Medical thresholds
if (bodyTemperature > 40.0) { // Hyperthermia - medical emergency
triggerAlert(CRITICAL, "Hyperthermia: Body temp " + String(bodyTemperature, 1) + "°C");
anyAlert = true;
} else if (bodyTemperature < 35.0) { // Hypothermia
triggerAlert(CRITICAL, "Hypothermia: Body temp " + String(bodyTemperature, 1) + "°C");
anyAlert = true;
} else if (bodyTemperature > maxBodyTemperature) { // Fever
triggerAlert(WARNING, "Fever: Body temp " + String(bodyTemperature, 1) + "°C");
anyAlert = true;
}
// Breathing Rate Alerts - Medical thresholds
if (breathingRate < 8) { // Severe bradypnea
triggerAlert(CRITICAL, "Severe Bradypnea: BR " + String((int)breathingRate) + " /min");
anyAlert = true;
} else if (breathingRate > 40) { // Severe tachypnea
triggerAlert(CRITICAL, "Severe Tachypnea: BR " + String((int)breathingRate) + " /min");
anyAlert = true;
} else if (breathingRate < minBreathingRate || breathingRate > maxBreathingRate) {
triggerAlert(WARNING, "Abnormal Breathing: " + String((int)breathingRate) + " /min");
anyAlert = true;
}
// Fall Detection - High acceleration threshold
if (acceleration > maxAcceleration) {
triggerAlert(CRITICAL, "Fall Detected: " + String(acceleration, 1) + "g acceleration");
anyAlert = true;
}
// Environmental temperature (ambient)
if (temperature > maxTemperature) {
triggerAlert(WARNING, "High Ambient Temp: " + String(temperature, 1) + "°C");
anyAlert = true;
}
// Clear LED if no alerts
if (!anyAlert) {
digitalWrite(RED_LED_PIN, LOW);
digitalWrite(GREEN_LED_PIN, HIGH);
delay(50);
digitalWrite(GREEN_LED_PIN, LOW);
noTone(BUZZER_PIN);
}
}
void publishSensorData() {
Serial.println("publishSensorData() called");
if (client.connected()) {
Serial.println("MQTT client connected, building JSON...");
StaticJsonDocument<4096> doc; // Increased buffer size
// Single JSON object with all sensor data
doc["timestamp"] = millis();
doc["device_id"] = "ESP32_001";
// Vital signs
doc["sensors"]["heart_rate"] = calculateAverage(heartRateBuffer, bufferFull ? 5 : bufferIndex);
doc["sensors"]["breathing_rate"] = currentBreathingRate;
doc["sensors"]["hrv"] = currentHRV;
// Environmental sensors
doc["sensors"]["temperature"] = calculateAverage(tempBuffer, bufferFull ? 5 : bufferIndex);
doc["sensors"]["humidity"] = dht.readHumidity();
doc["sensors"]["body_temperature"] = calculateAverage(bodyTempBuffer, bufferFull ? 5 : bufferIndex);
// Motion sensors
doc["sensors"]["accelerometer"]["x"] = mpu6050.getAccX();
doc["sensors"]["accelerometer"]["y"] = mpu6050.getAccY();
doc["sensors"]["accelerometer"]["z"] = mpu6050.getAccZ();
doc["sensors"]["accelerometer"]["total"] = sqrt(pow(mpu6050.getAccX(),2) + pow(mpu6050.getAccY(),2) + pow(mpu6050.getAccZ(),2));
// GPS data
doc["sensors"]["gps"]["latitude"] = gpsLatitude;
doc["sensors"]["gps"]["longitude"] = gpsLongitude;
doc["sensors"]["gps"]["speed"] = gpsSpeed;
doc["sensors"]["gps"]["elevation"] = currentElevation;
doc["sensors"]["gps"]["route_progress"] = routeProgress;
doc["sensors"]["gps"]["direction"] = movingForward ? "forward" : "backward";
doc["sensors"]["gps"]["status"] = String(gpsStatus);
doc["sensors"]["gps"]["valid"] = gpsDataValid;
// ECG data
doc["sensors"]["ecg"]["classification"] = ecgClassification;
doc["sensors"]["ecg"]["current_sample"] = ecgCurrentSample;
doc["sensors"]["ecg"]["p_wave_amplitude"] = ecgPWaveAmplitude;
doc["sensors"]["ecg"]["rhythm_regularity"] = ecgRhythmRegularity;
doc["sensors"]["ecg"]["qrs_amplitude"] = ecgQRSAmplitude;
doc["sensors"]["ecg"]["qrs_width"] = ecgQRSWidth;
// Device status
doc["sensors"]["battery_level"] = batteryLevel;
// Cycling context (new - useful for analytics)
doc["context"]["cycling_intensity"] = cyclingIntensity;
doc["context"]["exertion_level"] = currentExertionLevel;
if (automaticMode) {
doc["context"]["scenario"] = getScenarioName(currentScenario);
doc["context"]["scenario_progress"] = (float)scenarioElapsedTime / scenarioDurations[currentScenario];
}
// Units for reference
doc["units"]["heart_rate"] = "bpm";
doc["units"]["breathing_rate"] = "breaths/min";
doc["units"]["hrv"] = "ms";
doc["units"]["temperature"] = "°C";
doc["units"]["humidity"] = "%";
doc["units"]["body_temperature"] = "°C";
doc["units"]["battery_level"] = "%";
doc["units"]["accelerometer"] = "g";
doc["units"]["gps_speed"] = "km/h";
doc["units"]["gps_coords"] = "degrees";
doc["units"]["gps_elevation"] = "meters";
doc["units"]["ecg_sample"] = "mV";
doc["units"]["ecg_amplitude"] = "mV";
doc["units"]["ecg_width"] = "seconds";
String sensorDataJson;
size_t jsonSize = serializeJson(doc, sensorDataJson);
Serial.printf("JSON size: %d bytes\n", jsonSize);
if (jsonSize > 0) {
boolean result = client.publish(MQTT_TOPIC_DATA, sensorDataJson.c_str());
Serial.printf("MQTT publish result: %s\n", result ? "SUCCESS" : "FAILED");
if (result) {
Serial.println("Consolidated sensor data published to MQTT");
Serial.printf("Published to topic: %s\n", MQTT_TOPIC_DATA);
} else {
Serial.println("ERROR: MQTT publish failed!");
}
} else {
Serial.println("ERROR: JSON serialization failed!");
}
} else {
Serial.println("ERROR: MQTT client not connected!");
}
}
void publishAlert(const String& message, const String& severity) {
if (client.connected()) {
StaticJsonDocument<256> doc;
doc["timestamp"] = millis();
doc["device_id"] = "ESP32_001";
doc["alert_type"] = "threshold_exceeded";
doc["message"] = message;
doc["severity"] = severity;
String alertJson;
serializeJson(doc, alertJson);
client.publish(MQTT_TOPIC_ALERTS, alertJson.c_str());
}
}
void performHealthCheck() {
// Battery drain: 1% every 2 minutes
unsigned long currentTime = millis();
if (currentTime - lastBatteryDrain >= BATTERY_DRAIN_INTERVAL) {
batteryLevel -= 1.0; // Reduce by 1%
if (batteryLevel < 0) batteryLevel = 0;
lastBatteryDrain = currentTime;
}
// Check WiFi and MQTT status
wifiConnected = (WiFi.status() == WL_CONNECTED);
mqttConnected = client.connected();
// Publish health status
if (client.connected()) {
StaticJsonDocument<512> doc;
doc["timestamp"] = millis();
doc["device_id"] = "ESP32_001";
doc["battery_level"] = batteryLevel;
doc["wifi_connected"] = wifiConnected;
doc["mqtt_connected"] = mqttConnected;
doc["sensors_working"] = sensorsWorking;
doc["uptime"] = millis() / 1000;
String healthJson;
serializeJson(doc, healthJson);
client.publish(MQTT_TOPIC_HEALTH, healthJson.c_str());
Serial.printf("Health Check - Battery: %.1f%%, WiFi: %s, MQTT: %s, Sensors: %s\n",
batteryLevel, wifiConnected ? "OK" : "FAIL",
mqttConnected ? "OK" : "FAIL", sensorsWorking ? "OK" : "FAIL");
}
}
void updateDisplay() {
static unsigned long lastDisplayUpdate = 0;
if (millis() - lastDisplayUpdate > 2000) { // Update every 2 seconds
drawSmartWatchUI();
lastDisplayUpdate = millis();
}
}
void drawSmartWatchUI() {
tft.fillScreen(ILI9341_BLACK);
// Get current sensor values
float currentHR = calculateAverage(heartRateBuffer, bufferFull ? 5 : bufferIndex);
float currentTemp = calculateAverage(tempBuffer, bufferFull ? 5 : bufferIndex);
float currentBodyTemp = calculateAverage(bodyTempBuffer, bufferFull ? 5 : bufferIndex);
float currentHumidity = dht.readHumidity();
float currentAccel = sqrt(pow(mpu6050.getAccX(),2) + pow(mpu6050.getAccY(),2) + pow(mpu6050.getAccZ(),2));
// Header - removed for cleaner look
// Show control mode status
tft.setTextSize(1);
tft.setTextColor(ILI9341_YELLOW);
safeTftSetCursor(5, 5);
int16_t pulseValue = analogRead(PULSE_PIN);
float pulseHeartRate = readPulseSensor(pulseValue);
int breathingPotValue = analogRead(BREATHING_PIN);
int hrvPotValue = analogRead(HRV_PIN);
int speedPotValue = analogRead(SPEED_PIN);
// First line: HR and BR status
if (pulseHeartRate > 0) {
tft.printf("HR: MANUAL (%.0f) | BR: %s", pulseHeartRate,
(breathingPotValue <= ADC_AUTO_MODE_THRESHOLD) ? "AUTO" : "MANUAL");
} else {
tft.printf("HR: AUTO | BR: %s",
(breathingPotValue <= ADC_AUTO_MODE_THRESHOLD) ? "AUTO" : "MANUAL");
}
// Second line: HRV and Speed status
safeTftSetCursor(180, 5);
tft.printf("HRV: %s | SPD: %s",
(hrvPotValue <= ADC_AUTO_MODE_THRESHOLD) ? "AUTO" : "MANUAL",
(speedPotValue <= ADC_AUTO_MODE_THRESHOLD) ? "AUTO" : "MANUAL");
// Draw separator line after header
tft.drawLine(0, 18, 320, 18, ILI9341_CYAN);
// === VITAL SIGNS ROW (HR, BR, HRV) ===
drawMetricCard(5, 25, 100, 35, "HEART RATE", String((int)currentHR) + " bpm",
getHealthColor(currentHR, minHeartRate, maxHeartRate));
drawMetricCard(110, 25, 100, 35, "BREATHING", String((int)currentBreathingRate) + " /min",
getBreathingColor(currentBreathingRate, minBreathingRate, maxBreathingRate));
drawMetricCard(215, 25, 100, 35, "HRV", String((int)currentHRV) + " ms",
getHRVColor(currentHRV));
// === ENVIRONMENT ROW (Body Temp, Ambient Temp, Humidity, GPS Status) ===
drawMetricCard(5, 68, 75, 35, "BODY TEMP", String(currentBodyTemp, 1) + "C",
getTemperatureColor(currentBodyTemp, maxBodyTemperature));
drawMetricCard(85, 68, 75, 35, "AMBIENT", String(currentTemp, 1) + "C",
getTemperatureColor(currentTemp, maxTemperature));
drawMetricCard(165, 68, 90, 35, "HUMIDITY", String((int)currentHumidity) + "%",
ILI9341_CYAN);
// GPS Status moved to this row (next to humidity)
String statusDisplay = gpsDataValid ? "OK" : "SEARCH";
uint16_t statusColor = gpsDataValid ? ILI9341_GREEN : ILI9341_YELLOW;
drawMetricCard(260, 68, 55, 35, "GPS", statusDisplay, statusColor);
// === THIRD ROW (Acceleration and Speed) ===
drawMetricCard(5, 111, 155, 35, "ACC", String(currentAccel, 1) + "g",
getAccelColor(currentAccel, maxAcceleration));
// GPS Speed - show current speed (main speed display)
String speedDisplay = gpsDataValid ? String(gpsSpeed, 1) + " km/h" : "NO GPS";
uint16_t speedColor = gpsDataValid ? getSpeedColor(gpsSpeed) : ILI9341_YELLOW;
drawMetricCard(165, 111, 150, 35, "SPEED", speedDisplay, speedColor);
// === ECG AND SYSTEM STATUS ROW ===
// ECG Box (left side) - 60% of row width (186px)
uint16_t ecgColor = getECGClassificationColor(ecgClassification);
drawMetricCard(5, 154, 186, 30, "ECG", ecgClassification, ecgColor);
// Calculate system health percentage
int healthPercent = 0;
String healthIssues = "";
// Count working systems (out of 4: WiFi, MQTT, Sensors, GPS)
if (wifiConnected) healthPercent += 25;
else healthIssues += "WiFi ";
if (mqttConnected) healthPercent += 25;
else healthIssues += "MQTT ";
if (sensorsWorking) healthPercent += 25;
else healthIssues += "Sensors ";
if (gpsDataValid) healthPercent += 25;
else healthIssues += "GPS ";
// System Status Box (right side of ECG) - Two lines, 40% of row width
// Vertically centered within ECG box height (154-184px, center=169px)
// Top line: UPTIME (7px above center)
tft.setTextSize(1);
tft.setTextColor(ILI9341_WHITE);
safeTftSetCursor(196, 162);
tft.printf("UPTIME: %.0fs", millis()/1000.0);
// Bottom line: STATUS (7px below center)
safeTftSetCursor(196, 176);
tft.print("STATUS: ");
if (healthPercent == 100) {
tft.setTextColor(ILI9341_GREEN);
tft.print("100%");
} else {
tft.setTextColor(ILI9341_RED);
tft.printf("%d%% %s", healthPercent, healthIssues.c_str());
}
// === UNIFIED ALERT SECTION ===
drawAlerts(currentHR, currentTemp, currentBodyTemp, currentAccel, currentBreathingRate);
}
void drawAlerts(float hr, float temp, float bodyTemp, float accel, float breathing) {
String alertMsg = "";
uint16_t alertColor = ILI9341_GREEN;
bool hasHealthAlert = false;
bool isHighPriority = false;
// Check for high priority alerts first (critical health issues)
if (hr < 50 || hr > 200) { // Severe bradycardia or tachycardia
hasHealthAlert = true;
isHighPriority = true;
alertColor = ILI9341_RED;
if (hr < 50) alertMsg = "SEVERE BRADYCARDIA";
else alertMsg = "SEVERE TACHYCARDIA";
} else if (bodyTemp > 40.0 || bodyTemp < 35.0) { // Hyperthermia or hypothermia
hasHealthAlert = true;
isHighPriority = true;
alertColor = ILI9341_RED;
if (bodyTemp > 40.0) alertMsg = "HYPERTHERMIA";
else alertMsg = "HYPOTHERMIA";
} else if (breathing < 8 || breathing > 40) { // Severe breathing issues
hasHealthAlert = true;
isHighPriority = true;
alertColor = ILI9341_RED;
if (breathing < 8) alertMsg = "SEVERE BRADYPNEA";
else alertMsg = "SEVERE TACHYPNEA";
} else if (accel > maxAcceleration) { // Fall detection
hasHealthAlert = true;
isHighPriority = true;
alertColor = ILI9341_RED;
alertMsg = "FALL DETECTED";
}
// Check for moderate priority alerts
else if (hr < minHeartRate || hr > maxHeartRate) {
hasHealthAlert = true;
alertColor = ILI9341_YELLOW;
alertMsg = "HEART RATE ABNORMAL";
} else if (breathing < minBreathingRate || breathing > maxBreathingRate) {
hasHealthAlert = true;
alertColor = ILI9341_YELLOW;
alertMsg = "BREATHING ABNORMAL";
} else if (bodyTemp > maxBodyTemperature) {
hasHealthAlert = true;
alertColor = ILI9341_YELLOW;
alertMsg = "FEVER DETECTED";
} else if (temp > maxTemperature) {
hasHealthAlert = true;
alertColor = ILI9341_YELLOW;
alertMsg = "HIGH AMBIENT TEMP";
}
// Alert area positioning (bottom section of screen)
int alertY = 190;
int alertHeight = 45; // Bigger alert area
// Priority 1: Emergency Button (highest priority)
if (emergencyButtonPressed) {
// Fill background with red for emergency
safeTftFillRect(0, alertY, 320, alertHeight, ILI9341_RED);
// Draw emergency message with blinking effect
tft.setTextSize(3); // Larger text for emergency
unsigned long blinkTime = (millis() - emergencyButtonPressTime) % 1000;
if (blinkTime < 500) {
tft.setTextColor(ILI9341_WHITE);
} else {
tft.setTextColor(ILI9341_YELLOW); // Blink with yellow
}
// Center the emergency text properly
String emergencyMsg = "EMERGENCY SENT";
int textWidth = emergencyMsg.length() * 18; // Size 3 text ≈ 18px per char
int centerX = (320 - textWidth) / 2;
safeTftSetCursor(centerX, alertY + 12);
tft.print(emergencyMsg);
}
// Priority 2: Health Alerts (if no emergency)
else if (hasHealthAlert) {
// Fill background based on severity
uint16_t bgColor = isHighPriority ? ILI9341_RED : ILI9341_BLACK;
safeTftFillRect(0, alertY, 320, alertHeight, bgColor);
// Draw border
safeTftDrawRect(0, alertY, 320, alertHeight, alertColor);
// Large alert text
tft.setTextSize(2); // Bigger than before
tft.setTextColor(alertColor);
// Center the alert message - using ASCII-safe formatting
String displayMsg = "! " + alertMsg + " !";
int textWidth = displayMsg.length() * 12; // Approximate width for size 2
int centerX = (320 - textWidth) / 2;
safeTftSetCursor(centerX, alertY + 8);
tft.print(displayMsg);
// Add blinking effect for high priority alerts
if (isHighPriority) {
unsigned long blinkTime = millis() % 1000;
if (blinkTime < 500) {
tft.setTextColor(ILI9341_WHITE);
safeTftSetCursor(centerX, alertY + 8);
tft.print(displayMsg);
}
}
// Add additional info line for context
tft.setTextSize(1);
tft.setTextColor(ILI9341_WHITE);
String detailMsg = "";
if (alertMsg.indexOf("HEART") >= 0) detailMsg = "HR: " + String((int)hr) + " bpm";
else if (alertMsg.indexOf("BREATHING") >= 0) detailMsg = "BR: " + String((int)breathing) + " /min";
else if (alertMsg.indexOf("TEMP") >= 0 || alertMsg.indexOf("FEVER") >= 0) detailMsg = "Temp: " + String(bodyTemp, 1) + "°C";
else if (alertMsg.indexOf("FALL") >= 0) detailMsg = "G-Force: " + String(accel, 1) + "g";
if (detailMsg.length() > 0) {
int detailWidth = detailMsg.length() * 6;
int detailX = (320 - detailWidth) / 2;
safeTftSetCursor(detailX, alertY + 28);
tft.print(detailMsg);
}
}
// Priority 3: All Normal (lowest priority)
else {
// Show system normal status centered
tft.setTextSize(1);
tft.setTextColor(ILI9341_GREEN);
String normalMsg = "ALL SYSTEMS OK";
int textWidth = normalMsg.length() * 6; // Size 1 text ≈ 6px per char
int centerX = (320 - textWidth) / 2;
safeTftSetCursor(centerX, alertY + 15);
tft.print(normalMsg);
}
}
void drawMetricCard(int x, int y, int width, int height, String label, String value, uint16_t valueColor) {
// Draw card border with bounds checking
safeTftDrawRect(x, y, width, height, ILI9341_WHITE);
// Draw label - larger, more readable font
tft.setTextSize(1);
tft.setTextColor(ILI9341_WHITE);
safeTftSetCursor(x + 3, y + 3);
tft.print(label);
// Draw value with better centering and sizing
int textSize = 2;
int valueY = y + 18;
// Special handling for different box widths and content types
if (width <= 75) {
textSize = 2;
valueY = y + 16;
} else if (width <= 100) {
textSize = 2;
valueY = y + 18;
} else {
// For wider boxes, adjust based on height
if (width >= 150) {
if (height <= 35) {
// Shorter boxes like ECG (150x30) - position text higher
textSize = 2;
valueY = y + 15; // Higher positioning for shallow boxes
} else {
// Taller boxes - use normal positioning
textSize = 2;
valueY = y + 19;
}
} else {
textSize = 2;
valueY = y + 18;
}
}
tft.setTextSize(textSize);
tft.setTextColor(valueColor);
// Calculate horizontal centering for text
int charWidth = 6 * textSize; // Approximate character width
int textWidth = value.length() * charWidth;
int centeredX = x + (width - textWidth) / 2;
// Ensure minimum left margin
if (centeredX < x + 3) centeredX = x + 3;
safeTftSetCursor(centeredX, valueY);
tft.print(value);
}
void drawExertionIndicator(int x, int y, String level, float intensity) {
// Draw exertion level label
tft.setTextSize(1);
tft.setTextColor(ILI9341_WHITE);
tft.setCursor(x, y);
tft.print("EXERTION: ");
// Color code the exertion level
uint16_t levelColor = getExertionColor(level);
tft.setTextColor(levelColor);
tft.print(level);
// Draw intensity bar
int barWidth = 200;
int barHeight = 6;
int barX = x + 80;
int barY = y;
// Draw bar outline
tft.drawRect(barX, barY, barWidth, barHeight, ILI9341_WHITE);
// Fill intensity bar
int fillWidth = (int)(intensity * (barWidth - 2));
if (fillWidth > 0) {
tft.fillRect(barX + 1, barY + 1, fillWidth, barHeight - 2, levelColor);
}
// Show percentage
tft.setTextColor(ILI9341_WHITE);
tft.setCursor(barX + barWidth + 5, barY);
tft.printf("%.0f%%", intensity * 100);
}
void drawScenarioProgress(int x, int y, String scenarioName, unsigned long elapsed, int duration) {
// Draw scenario name
tft.setTextSize(1);
tft.setTextColor(ILI9341_CYAN);
tft.setCursor(x, y);
tft.print("WORKOUT: ");
tft.setTextColor(ILI9341_WHITE);
tft.print(scenarioName);
// Draw progress bar
int barWidth = 150;
int barHeight = 4;
int barX = x + 200;
int barY = y + 1;
// Calculate progress
float progress = (float)elapsed / duration;
if (progress > 1.0) progress = 1.0;
// Draw bar outline
tft.drawRect(barX, barY, barWidth, barHeight, ILI9341_WHITE);
// Fill progress bar
int fillWidth = (int)(progress * (barWidth - 2));
if (fillWidth > 0) {
tft.fillRect(barX + 1, barY + 1, fillWidth, barHeight - 2, ILI9341_CYAN);
}
// Show time remaining
int remainingMin = (duration - elapsed) / 60;
int remainingSec = (duration - elapsed) % 60;
tft.setTextColor(ILI9341_WHITE);
tft.setCursor(x, y + 8);
tft.printf("Time: %02d:%02d remaining", remainingMin, remainingSec);
}
void drawBatteryIndicator(int x, int y, float batteryLevel) {
// Draw battery outline (phone-style)
int width = 40;
int height = 12;
tft.drawRect(x, y, width, height, ILI9341_WHITE);
// Battery tip
tft.fillRect(x + width, y + 3, 3, 6, ILI9341_WHITE);
// Fill level based on battery percentage
int fillWidth = (int)((batteryLevel / 100.0) * (width - 2));
uint16_t fillColor;
if (batteryLevel > 50) fillColor = ILI9341_GREEN;
else if (batteryLevel > 20) fillColor = ILI9341_YELLOW;
else fillColor = ILI9341_RED;
if (fillWidth > 0) {
tft.fillRect(x + 1, y + 1, fillWidth, height - 2, fillColor);
}
// Battery percentage text
tft.setTextSize(1);
tft.setTextColor(ILI9341_WHITE);
tft.setCursor(x + width + 8, y + 2);
tft.printf("%.0f%%", batteryLevel);
}
uint16_t getHealthColor(float value, float minVal, float maxVal) {
if (value < minVal || value > maxVal) return ILI9341_RED;
if (value < minVal * 1.1 || value > maxVal * 0.9) return ILI9341_YELLOW;
return ILI9341_GREEN;
}
uint16_t getBreathingColor(float breathing, float minBreathing, float maxBreathing) {
if (breathing < minBreathing || breathing > maxBreathing) return ILI9341_RED;
if (breathing < minBreathing * 1.1 || breathing > maxBreathing * 0.9) return ILI9341_YELLOW;
return ILI9341_GREEN;
}
uint16_t getTemperatureColor(float temp, float maxTemp) {
if (temp > maxTemp) return ILI9341_RED;
if (temp > maxTemp * 0.9) return ILI9341_YELLOW;
return ILI9341_GREEN;
}
uint16_t getAccelColor(float accel, float maxAccel) {
if (accel > maxAccel) return ILI9341_RED;
if (accel > maxAccel * 0.8) return ILI9341_YELLOW;
return ILI9341_GREEN;
}
uint16_t getBatteryColor(float battery) {
if (battery < 20) return ILI9341_RED;
if (battery < 50) return ILI9341_YELLOW;
return ILI9341_GREEN;
}
uint16_t getExertionColor(String level) {
if (level == "Rest") return ILI9341_GREEN;
if (level == "Light") return ILI9341_CYAN;
if (level == "Moderate") return ILI9341_YELLOW;
if (level == "Intense") return 0xFD20; // Orange color (RGB565)
if (level == "Maximum") return ILI9341_RED;
return ILI9341_WHITE;
}
uint16_t getHRVColor(float hrv) {
// HRV color coding: Low HRV = Red (stress), Normal HRV = Green, High HRV = Cyan (very relaxed)
if (hrv < 30) return ILI9341_RED; // Low HRV (stress/fatigue)
if (hrv < 60) return ILI9341_YELLOW; // Moderate HRV
if (hrv < 100) return ILI9341_GREEN; // Good HRV (healthy)
return ILI9341_CYAN; // High HRV (very relaxed/athletic)
}
uint16_t getSpeedColor(float speed) {
// Speed color coding: Very low = Red (stopped), Low = Yellow, Normal cycling = Green, High = Cyan
if (speed < 5.0) return ILI9341_RED; // Very low speed (stopped/walking)
if (speed < 15.0) return ILI9341_YELLOW; // Low speed (slow cycling)
if (speed < 35.0) return ILI9341_GREEN; // Normal cycling speed
return ILI9341_CYAN; // High speed (fast cycling)
}
uint16_t getECGClassificationColor(String classification) {
if (classification == "Normal") return ILI9341_GREEN;
if (classification == "Brady") return ILI9341_YELLOW;
if (classification == "AFib") return ILI9341_RED;
if (classification == "Noisy") return ILI9341_MAGENTA;
return ILI9341_WHITE; // Unknown
}
// Safe TFT drawing functions with bounds checking
void safeTftSetCursor(int x, int y) {
if (x >= 0 && x < TFT_WIDTH && y >= 0 && y < TFT_HEIGHT) {
tft.setCursor(x, y);
}
}
void safeTftDrawRect(int x, int y, int width, int height, uint16_t color) {
// Clamp coordinates and dimensions to screen bounds
if (x < 0) { width += x; x = 0; }
if (y < 0) { height += y; y = 0; }
if (x + width > TFT_WIDTH) width = TFT_WIDTH - x;
if (y + height > TFT_HEIGHT) height = TFT_HEIGHT - y;
if (width > 0 && height > 0) {
tft.drawRect(x, y, width, height, color);
}
}
void safeTftFillRect(int x, int y, int width, int height, uint16_t color) {
// Clamp coordinates and dimensions to screen bounds
if (x < 0) { width += x; x = 0; }
if (y < 0) { height += y; y = 0; }
if (x + width > TFT_WIDTH) width = TFT_WIDTH - x;
if (y + height > TFT_HEIGHT) height = TFT_HEIGHT - y;
if (width > 0 && height > 0) {
tft.fillRect(x, y, width, height, color);
}
}
void drawAlertStatus(float hr, float temp, float bodyTemp, float accel, float breathing) {
bool hasAlert = false;
String alertMsg = "";
// Check for any alerts
if (hr < minHeartRate || hr > maxHeartRate) {
hasAlert = true;
alertMsg = "HEART RATE ALERT";
} else if (breathing < minBreathingRate || breathing > maxBreathingRate) {
hasAlert = true;
alertMsg = "BREATHING ALERT";
} else if (bodyTemp > maxBodyTemperature) {
hasAlert = true;
alertMsg = "HIGH BODY TEMP";
} else if (temp > maxTemperature) {
hasAlert = true;
alertMsg = "HIGH AMBIENT TEMP";
} else if (accel > maxAcceleration) {
hasAlert = true;
alertMsg = "FALL DETECTED";
}
// Draw alert status bar (alerts only - system status moved up)
if (hasAlert) {
tft.setTextSize(1);
safeTftSetCursor(5, 205);
tft.setTextColor(ILI9341_WHITE);
tft.print("ALERT: ");
tft.setTextColor(ILI9341_RED);
tft.print(alertMsg);
}
}
void connectToWiFi() {
WiFi.begin(ssid, password);
tft.fillScreen(ILI9341_BLACK);
tft.setCursor(0, 0);
tft.print("Connecting WiFi");
lastWifiAttempt = millis();
wifiAttempts = 0;
}
void checkWiFiConnection() {
if (WiFi.status() == WL_CONNECTED) {
if (!wifiConnected) {
Serial.println("\nWiFi connected successfully!");
Serial.printf("IP address: %s\n", WiFi.localIP().toString().c_str());
tft.fillScreen(ILI9341_BLACK);
tft.setCursor(0, 0);
tft.print("WiFi Connected");
tft.setCursor(0, 1);
tft.print(WiFi.localIP());
wifiConnected = true;
}
return;
}
// Non-blocking WiFi connection retry
unsigned long currentTime = millis();
if (wifiAttempts < MAX_WIFI_ATTEMPTS &&
currentTime - lastWifiAttempt >= WIFI_RETRY_INTERVAL) {
Serial.print(".");
tft.setCursor(0, 1);
tft.printf("Attempt %d/%d", wifiAttempts + 1, MAX_WIFI_ATTEMPTS);
wifiAttempts++;
lastWifiAttempt = currentTime;
} else if (wifiAttempts >= MAX_WIFI_ATTEMPTS && !wifiConnected) {
Serial.println("\nWiFi connection failed!");
tft.fillScreen(ILI9341_BLACK);
tft.setCursor(0, 0);
tft.print("WiFi Failed");
wifiConnected = false;
}
}
void reconnectMQTT() {
while (!client.connected()) {
Serial.print("Attempting MQTT connection...");
if (client.connect("ESP32_IoTWearable_001")) {
Serial.println(" connected!");
// Subscribe to control topics if needed
// client.subscribe("cyclists/user001/control/#");
} else {
Serial.printf(" failed, rc=%d. Retrying in 5 seconds...\n", client.state());
delay(5000);
}
}
}