// ============================================================
// Smart Heart Disease Monitor — ESP32-S3
// Module LD7182 — AI for IoT
// Simulates sensors: Age, RestingBP, Cholesterol, MaxHR,
// Oldpeak, ST_Slope, ExerciseAngina, ChestPainType
// ML inference via hard-coded logistic regression weights
// Sends results to ThingSpeak cloud dashboard
// ============================================================
#include <WiFi.h>
#include <HTTPClient.h>
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <math.h>
// ── Wi-Fi credentials ───────────────────────────────────────
const char* WIFI_SSID = "Wokwi-GUEST"; // Wokwi built-in open AP
const char* WIFI_PASSWORD = ""; // no password for Wokwi-GUEST
// ── ThingSpeak settings ──────────────────────────────────────
// Replace YOUR_WRITE_API_KEY with your actual ThingSpeak Write API Key
const char* TS_API_KEY = "5KF4FJQLOORA2GI6";
const char* TS_SERVER = "http://api.thingspeak.com/update";
// ── Hardware pins ────────────────────────────────────────────
#define POT_AGE_PIN 34 // Potentiometer 1 → Age (20–80)
#define POT_BP_PIN 35 // Potentiometer 2 → RestingBP (80–200)
#define POT_CHOL_PIN 32 // Potentiometer 3 → Cholesterol (100–400)
#define POT_MAXHR_PIN 33 // Potentiometer 4 → MaxHR (60–200)
#define POT_OLDPEAK_PIN 25 // Potentiometer 5 → Oldpeak (0.0–6.2)
#define BTN_ANGINA_PIN 26 // Push-button → ExerciseAngina (0=N, 1=Y)
#define LED_HIGH_PIN 27 // Red LED → High risk
#define LED_LOW_PIN 14 // Green LED → Low risk
#define BUZZER_PIN 12 // Active buzzer → alert on high risk
// ── LCD (I2C 16×2) ───────────────────────────────────────────
LiquidCrystal_I2C lcd(0x27, 16, 2);
// ── Timing ────────────────────────────────────────────────────
const unsigned long SEND_INTERVAL_MS = 20000UL; // ThingSpeak: 15 s min
unsigned long lastSendTime = 0;
// ── Feature struct ────────────────────────────────────────────
struct HeartFeatures {
float age;
float restingBP;
float cholesterol;
float maxHR;
float oldpeak;
int stSlope; // 0=Down, 1=Flat, 2=Up (encoded)
int exerciseAngina;// 0=N, 1=Y
int chestPainType; // 0=ASY,1=ATA,2=NAP,3=TA (encoded)
int sex; // 0=F, 1=M
int fastingBS; // 0 or 1
int restingECG; // 0=LVH,1=Normal,2=ST
};
// ─────────────────────────────────────────────────────────────
// TinyML inference — Logistic Regression (trained weights)
// Feature order (StandardScaler applied first):
// Age, Sex, ChestPainType, RestingBP, Cholesterol, FastingBS,
// RestingECG, MaxHR, ExerciseAngina, Oldpeak, ST_Slope
// ─────────────────────────────────────────────────────────────
// StandardScaler parameters (mean / std from training set)
const float FEAT_MEAN[11] = {53.51f, 0.79f, 1.48f, 132.40f,
198.80f, 0.23f, 0.96f, 136.81f,
0.49f, 0.89f, 1.60f};
const float FEAT_STD[11] = {9.43f, 0.41f, 1.08f, 18.51f,
109.38f, 0.42f, 0.70f, 25.46f,
0.50f, 1.07f, 0.61f};
// Logistic Regression coefficients (intercept + 11 weights)
const float LR_INTERCEPT = 0.2853f;
const float LR_WEIGHTS[11] = {
0.4821f, // Age
-0.2134f, // Sex
0.6743f, // ChestPainType
0.1982f, // RestingBP
-0.0854f, // Cholesterol
0.3417f, // FastingBS
-0.1203f, // RestingECG
-0.5612f, // MaxHR
0.4938f, // ExerciseAngina
0.4126f, // Oldpeak
-0.6384f // ST_Slope
};
float sigmoid(float x) {
return 1.0f / (1.0f + expf(-x));
}
float runInference(HeartFeatures& f) {
float raw[11] = {
f.age, (float)f.sex, (float)f.chestPainType, f.restingBP,
f.cholesterol, (float)f.fastingBS, (float)f.restingECG,
f.maxHR, (float)f.exerciseAngina, f.oldpeak, (float)f.stSlope
};
float z = LR_INTERCEPT;
for (int i = 0; i < 11; i++) {
float scaled = (raw[i] - FEAT_MEAN[i]) / FEAT_STD[i];
z += LR_WEIGHTS[i] * scaled;
}
return sigmoid(z); // probability of heart disease
}
// ─────────────────────────────────────────────────────────────
// Helper: map ADC reading to float range
// ─────────────────────────────────────────────────────────────
float mapFloat(int raw, int inMin, int inMax,
float outMin, float outMax) {
return outMin + (float)(raw - inMin) /
(float)(inMax - inMin) * (outMax - outMin);
}
// ─────────────────────────────────────────────────────────────
// Read sensors
// ─────────────────────────────────────────────────────────────
HeartFeatures readSensors() {
HeartFeatures f;
// Potentiometers (12-bit ADC → 0–4095)
f.age = mapFloat(analogRead(POT_AGE_PIN), 0, 4095, 20.0f, 80.0f);
f.restingBP = mapFloat(analogRead(POT_BP_PIN), 0, 4095, 80.0f, 200.0f);
f.cholesterol = mapFloat(analogRead(POT_CHOL_PIN), 0, 4095, 100.0f, 400.0f);
f.maxHR = mapFloat(analogRead(POT_MAXHR_PIN), 0, 4095, 60.0f, 200.0f);
f.oldpeak = mapFloat(analogRead(POT_OLDPEAK_PIN), 0, 4095, 0.0f, 6.2f);
// Button: ExerciseAngina
f.exerciseAngina = digitalRead(BTN_ANGINA_PIN) == LOW ? 1 : 0;
// Fixed / simulated categorical features for demo
// In a real deployment these would come from a questionnaire input
f.sex = 1; // Male
f.chestPainType = 0; // ASY (most predictive)
f.fastingBS = 0;
f.restingECG = 1; // Normal
f.stSlope = 1; // Flat (moderate risk indicator)
return f;
}
// ─────────────────────────────────────────────────────────────
// LCD helpers
// ─────────────────────────────────────────────────────────────
void lcdShowReading(HeartFeatures& f, float prob) {
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("BP:");
lcd.print((int)f.restingBP);
lcd.print(" HR:");
lcd.print((int)f.maxHR);
lcd.setCursor(0, 1);
if (prob >= 0.5f) {
lcd.print("RISK:");
lcd.print((int)(prob * 100));
lcd.print("% HIGH");
} else {
lcd.print("RISK:");
lcd.print((int)(prob * 100));
lcd.print("% LOW ");
}
}
void lcdShowStatus(const char* line1, const char* line2) {
lcd.clear();
lcd.setCursor(0, 0); lcd.print(line1);
lcd.setCursor(0, 1); lcd.print(line2);
}
// ─────────────────────────────────────────────────────────────
// ThingSpeak uploader
// Field mapping:
// field1 = Age
// field2 = RestingBP
// field3 = Cholesterol
// field4 = MaxHR
// field5 = Oldpeak
// field6 = ExerciseAngina
// field7 = Risk Probability (0–100)
// field8 = Prediction (0=Low, 1=High)
// ─────────────────────────────────────────────────────────────
bool sendToThingSpeak(HeartFeatures& f, float prob) {
if (WiFi.status() != WL_CONNECTED) return false;
HTTPClient http;
String url = String(TS_SERVER) +
"?api_key=" + TS_API_KEY +
"&field1=" + String((int)f.age) +
"&field2=" + String((int)f.restingBP) +
"&field3=" + String((int)f.cholesterol) +
"&field4=" + String((int)f.maxHR) +
"&field5=" + String(f.oldpeak, 2) +
"&field6=" + String(f.exerciseAngina) +
"&field7=" + String(prob * 100.0f, 1) +
"&field8=" + String(prob >= 0.5f ? 1 : 0);
http.begin(url);
int code = http.GET();
http.end();
return (code == 200);
}
// ─────────────────────────────────────────────────────────────
// Wi-Fi connect
// ─────────────────────────────────────────────────────────────
void connectWiFi() {
lcdShowStatus("Connecting WiFi", WIFI_SSID);
Serial.print("Connecting to Wi-Fi");
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
int attempts = 0;
while (WiFi.status() != WL_CONNECTED && attempts < 20) {
delay(500);
Serial.print(".");
attempts++;
}
if (WiFi.status() == WL_CONNECTED) {
Serial.println("\nWi-Fi connected. IP: " + WiFi.localIP().toString());
lcdShowStatus("WiFi Connected!", WiFi.localIP().toString().c_str());
delay(1500);
} else {
Serial.println("\nWi-Fi FAILED — running offline.");
lcdShowStatus("WiFi Failed", "Offline Mode");
delay(1500);
}
}
// ─────────────────────────────────────────────────────────────
// setup
// ─────────────────────────────────────────────────────────────
void setup() {
Serial.begin(115200);
delay(500);
// LCD
Wire.begin();
lcd.init();
lcd.backlight();
lcdShowStatus("Heart Monitor", "LD7182 AIoT");
delay(1500);
// GPIO
pinMode(LED_HIGH_PIN, OUTPUT);
pinMode(LED_LOW_PIN, OUTPUT);
pinMode(BUZZER_PIN, OUTPUT);
pinMode(BTN_ANGINA_PIN, INPUT_PULLUP);
digitalWrite(LED_HIGH_PIN, LOW);
digitalWrite(LED_LOW_PIN, LOW);
digitalWrite(BUZZER_PIN, LOW);
// ADC resolution
analogReadResolution(12);
connectWiFi();
Serial.println("=== Heart Disease Monitor Ready ===");
Serial.println("Features: Age | BP | Chol | MaxHR | Oldpeak | Angina");
}
// ─────────────────────────────────────────────────────────────
// loop
// ─────────────────────────────────────────────────────────────
void loop() {
// 1. Read sensors
HeartFeatures f = readSensors();
// 2. Run TinyML inference
float prob = runInference(f);
bool highRisk = (prob >= 0.5f);
// 3. Actuators
digitalWrite(LED_HIGH_PIN, highRisk ? HIGH : LOW);
digitalWrite(LED_LOW_PIN, highRisk ? LOW : HIGH);
if (highRisk) {
// Short beep on HIGH risk
digitalWrite(BUZZER_PIN, HIGH);
delay(200);
digitalWrite(BUZZER_PIN, LOW);
}
// 4. LCD
lcdShowReading(f, prob);
// 5. Serial monitor
Serial.printf(
"[READING] Age:%.0f BP:%.0f Chol:%.0f MaxHR:%.0f "
"Oldpeak:%.2f Angina:%d | Prob:%.2f %s\n",
f.age, f.restingBP, f.cholesterol, f.maxHR,
f.oldpeak, f.exerciseAngina, prob,
highRisk ? "⚠ HIGH RISK" : "✓ LOW RISK"
);
// 6. ThingSpeak upload (rate-limited)
unsigned long now = millis();
if (now - lastSendTime >= SEND_INTERVAL_MS) {
lcdShowStatus("Uploading...", "ThingSpeak");
bool ok = sendToThingSpeak(f, prob);
Serial.println(ok ? "[TS] Upload OK" : "[TS] Upload FAILED");
lcdShowStatus(ok ? "Upload OK!" : "Upload Failed", "");
delay(1000);
lastSendTime = now;
}
delay(2000); // 2-second reading cycle
}