#include #include #include #include #include #include #include #include #include #include #include "spo2_algorithm.h" #define TFT_CS 5 #define TFT_RST 4 #define TFT_DC 2 #define ONE_WIRE_BUS 13 const char* ssid = "sembarang"; const char* password = "12345678"; const char* serverName = "http://10.107.21.239/iot/save_data.php"; Adafruit_ILI9341 tft(TFT_CS, TFT_DC, TFT_RST); MAX30105 particleSensor; OneWire oneWire(ONE_WIRE_BUS); DallasTemperature ds18b20(&oneWire); enum State { WAIT_FINGER, MEASURE_BPM, WAIT_TEMP_STABILIZE, MEASURE_TEMP, SHOW_RESULT }; State currentState = WAIT_FINGER; const byte RATE_SIZE = 8; byte rates[RATE_SIZE]; byte rateSpot = 0; byte validRateCount = 0; long lastBeat = 0; int beatAvg = 0; int lastStoredBeatAvg = 0; long lastIR = 0; float suhuCelcius = 0; unsigned long stateStart = 0; unsigned long lastTempRead = 0; unsigned long lastSerial = 0; float bpmFilteredSum = 0; int bpmFilteredCount = 0; float tempSum = 0; int tempCount = 0; int finalBPM = 0; float finalTemp = 0; int finalSpO2 = 0; int systolic = 0; int diastolic = 0; bool resultSent = false; // Heart Animation bool heartBeatAnimation = false; unsigned long heartAnimStart = 0; // Variabel baru untuk stabilisasi suhu float initialTemp = 0; bool tempStabilized = false; unsigned long tempStableStart = 0; const float TEMP_RISE_THRESHOLD = 0.5; // SpO2 Variables uint32_t irBuffer[100]; uint32_t redBuffer[100]; int32_t bufferLength = 100; int32_t spo2 = 0; int8_t validSPO2 = 0; int32_t heartRateSpO2 = 0; int8_t validHeartRate = 0; int spo2Sum = 0; int spo2Count = 0; // Countdown unsigned long lastCountdownUpdate = 0; void showMessage(const char* line1, const char* line2, const char* line3 = "") { tft.fillScreen(ILI9341_BLACK); tft.setTextColor(ILI9341_YELLOW); tft.setTextSize(2); tft.setCursor(15, 50); tft.println(line1); tft.setTextSize(1); tft.setCursor(15, 100); tft.println(line2); tft.setCursor(15, 120); tft.println(line3); } // ==================== UPDATE STATUS ==================== String getStatusBPM(int bpm) { if (bpm < 60) return "Rendah"; if (bpm > 100) return "Tinggi"; return "Normal"; } String getStatusTemp(float temp) { if (temp < 35.5) return "Rendah"; if (temp > 38.0) return "Tinggi"; return "Normal"; } String getStatusSpO2(int spo2Val) { if (spo2Val < 95) return "Rendah"; if (spo2Val > 100) return "Tinggi"; return "Normal"; } String getStatusBP(int sys, int dia) { if (sys < 90 || dia < 60) return "Rendah"; if (sys > 120 || dia > 80) return "Tinggi"; return "Normal"; } void showFinalResult() { tft.fillScreen(ILI9341_BLACK); tft.setTextSize(2); tft.setTextColor(ILI9341_GREEN); tft.setCursor(45, 15); tft.println("HASIL AKHIR"); tft.setTextSize(2); tft.setTextColor(ILI9341_WHITE); tft.setCursor(20, 55); tft.print("BPM : "); tft.print(finalBPM); tft.print(" "); tft.setTextColor(ILI9341_CYAN); tft.println(getStatusBPM(finalBPM)); tft.setCursor(20, 80); tft.setTextColor(ILI9341_WHITE); tft.print("SpO2 : "); tft.print(finalSpO2); tft.print("% "); tft.setTextColor(ILI9341_CYAN); tft.println(getStatusSpO2(finalSpO2)); tft.setCursor(20, 105); tft.setTextColor(ILI9341_WHITE); tft.print("TEMP : "); tft.print(finalTemp, 1); tft.print(" C "); tft.setTextColor(ILI9341_CYAN); tft.println(getStatusTemp(finalTemp)); tft.setCursor(20, 130); tft.setTextColor(ILI9341_WHITE); tft.print("BP : "); tft.print(systolic); tft.print("/"); tft.print(diastolic); tft.print(" "); tft.setTextColor(ILI9341_CYAN); tft.println(getStatusBP(systolic, diastolic)); } void showTempWaitingScreen() { tft.fillScreen(ILI9341_BLACK); tft.setTextColor(ILI9341_GREEN); tft.setTextSize(2); tft.setCursor(30, 20); tft.println("MENGUKUR SUHU"); tft.setTextColor(ILI9341_WHITE); tft.setCursor(20, 70); tft.print("BPM : "); tft.println(finalBPM); tft.setCursor(20, 100); tft.print("SpO2: "); tft.print(finalSpO2); tft.println("%"); tft.setCursor(20, 145); tft.setTextColor(ILI9341_YELLOW); tft.println("Genggam Tabung Besi"); } void drawHeart(bool big) { uint16_t color = ILI9341_RED; tft.fillRect(240, 10, 70, 60, ILI9341_BLACK); if (big) { tft.fillCircle(255, 22, 10, color); tft.fillCircle(275, 22, 10, color); tft.fillTriangle(245, 28, 285, 28, 265, 58, color); } else { tft.fillCircle(257, 24, 7, color); tft.fillCircle(273, 24, 7, color); tft.fillTriangle(250, 28, 280, 28, 265, 50, color); } } void setup() { Serial.begin(115200); WiFi.begin(ssid, password); tft.begin(); tft.setRotation(1); tft.fillScreen(ILI9341_BLACK); ds18b20.begin(); if (!particleSensor.begin(Wire, I2C_SPEED_FAST)) { showMessage("MAX30102 ERROR", "Check Sensor"); while (true); } particleSensor.setup(0x5F, 8, 2, 400, 411, 4096); particleSensor.setPulseAmplitudeRed(0x1F); particleSensor.setPulseAmplitudeIR(0x1F); showMessage("TEMPEL JARI", "Pada cahaya merah", "Menunggu BPM..."); } void loop() { readBPMRealtime(); readTemperatureRealtime(); if (currentState == MEASURE_BPM) { if (heartBeatAnimation) { drawHeart(true); if (millis() - heartAnimStart > 180) heartBeatAnimation = false; } else { drawHeart(false); } } if (millis() - lastSerial > 1000) { lastSerial = millis(); Serial.print("IR: "); Serial.print(lastIR); Serial.print(" | BPM: "); Serial.print(beatAvg); Serial.print(" | SpO2: "); Serial.print(spo2); Serial.print(" | TEMP: "); Serial.println(suhuCelcius, 1); } switch (currentState) { case WAIT_FINGER: if (beatAvg > 0) { bpmFilteredSum = 0; bpmFilteredCount = 0; spo2Sum = 0; spo2Count = 0; lastStoredBeatAvg = 0; stateStart = millis(); lastCountdownUpdate = millis(); currentState = MEASURE_BPM; showMessage("MENGUKUR BPM", "Jangan lepaskan jari", "40 detik..."); } break; case MEASURE_BPM: if (millis() - lastCountdownUpdate >= 1000) { lastCountdownUpdate = millis(); int sisa = 40 - ((millis() - stateStart) / 1000); if (sisa < 0) sisa = 0; tft.fillRect(20, 140, 200, 30, ILI9341_BLACK); tft.setCursor(20, 140); tft.setTextColor(ILI9341_CYAN); tft.setTextSize(2); tft.print("Sisa: "); tft.print(sisa); tft.println(" detik"); } if (beatAvg > 0 && beatAvg != lastStoredBeatAvg) { Serial.print("BEATAVG BARU = "); Serial.println(beatAvg); lastStoredBeatAvg = beatAvg; if (bpmFilteredCount == 0) { bpmFilteredSum += beatAvg; bpmFilteredCount++; } else { float runningAvg = bpmFilteredSum / bpmFilteredCount; if (abs(beatAvg - runningAvg) <= 10) { bpmFilteredSum += beatAvg; bpmFilteredCount++; } } } if (millis() - stateStart >= 40000UL) { finalBPM = (bpmFilteredCount > 0) ? round(bpmFilteredSum / bpmFilteredCount) : 0; finalSpO2 = (spo2Count > 5) ? round((float)spo2Sum / spo2Count) : 0; initialTemp = suhuCelcius; tempStabilized = false; tempSum = 0; tempCount = 0; stateStart = millis(); lastCountdownUpdate = millis(); currentState = WAIT_TEMP_STABILIZE; showMessage("MENGUKUR SUHU", "Menunggu Suhu Stabil", ""); } break; case WAIT_TEMP_STABILIZE: if (suhuCelcius > initialTemp + TEMP_RISE_THRESHOLD) { if (!tempStabilized) { tempStabilized = true; tempStableStart = millis(); } } if (tempStabilized && (millis() - tempStableStart >= 5000UL)) { stateStart = millis(); lastCountdownUpdate = millis(); currentState = MEASURE_TEMP; showTempWaitingScreen(); } if (millis() - stateStart >= 25000UL) { stateStart = millis(); lastCountdownUpdate = millis(); currentState = MEASURE_TEMP; showTempWaitingScreen(); } break; case MEASURE_TEMP: tempSum += suhuCelcius; tempCount++; if (millis() - lastCountdownUpdate >= 1000) { lastCountdownUpdate = millis(); int sisa = 40 - ((millis() - stateStart) / 1000); if (sisa < 0) sisa = 0; tft.fillRect(20, 200, 200, 30, ILI9341_BLACK); tft.setCursor(20, 200); tft.setTextColor(ILI9341_CYAN); tft.setTextSize(2); tft.print("Sisa: "); tft.print(sisa); tft.println(" dtk"); } if (millis() - stateStart >= 40000UL) { finalTemp = (tempCount > 0) ? tempSum / tempCount : suhuCelcius; currentState = SHOW_RESULT; showFinalResult(); } break; case SHOW_RESULT: if (!resultSent) { resultSent = true; sendDataToServer(finalBPM, finalTemp); } break; } delay(10); } void readBPMRealtime() { long irValue = particleSensor.getIR(); long redValue = particleSensor.getRed(); if (lastIR == 0) lastIR = irValue; long filteredIR = (irValue * 0.3) + (lastIR * 0.7); lastIR = filteredIR; static int bufferIndex = 0; if (currentState == MEASURE_BPM) { redBuffer[bufferIndex] = redValue; irBuffer[bufferIndex] = irValue; bufferIndex++; if (bufferIndex >= bufferLength) { maxim_heart_rate_and_oxygen_saturation(irBuffer, bufferLength, redBuffer, &spo2, &validSPO2, &heartRateSpO2, &validHeartRate); if (validSPO2 && spo2 > 70 && spo2 <= 100) { spo2Sum += spo2; spo2Count++; } bufferIndex = 0; } } if (filteredIR > 25000) { if (checkForBeat(filteredIR)) { heartBeatAnimation = true; heartAnimStart = millis(); long delta = millis() - lastBeat; lastBeat = millis(); float bpm_raw = 60.0 / (delta / 1000.0); if (delta < 450 || delta > 1200) return; if (bpm_raw > 50 && bpm_raw < 150) { static float lastValidBPM = 0; if (lastValidBPM > 0 && abs(bpm_raw - lastValidBPM) > 8) return; lastValidBPM = bpm_raw; rates[rateSpot++] = (byte)bpm_raw; rateSpot %= RATE_SIZE; if (validRateCount < RATE_SIZE) validRateCount++; beatAvg = 0; for (byte x = 0; x < validRateCount; x++) beatAvg += rates[x]; beatAvg /= validRateCount; } } } } void readTemperatureRealtime() { if (millis() - lastTempRead >= 2000) { lastTempRead = millis(); ds18b20.requestTemperatures(); float t = ds18b20.getTempCByIndex(0); if (t > 10 && t < 50) suhuCelcius = t + 1.5; } } void sendDataToServer(int bpm, float temp) { if (WiFi.status() != WL_CONNECTED) return; HTTPClient http; String url = "http://10.107.21.239/iot/save_data.php?action=insert" "&bpm=" + String(bpm) + "&spo2=" + String(finalSpO2) + "&temperature=" + String(temp, 1) + "&systolic=120" "&diastolic=80"; Serial.println(url); http.begin(url); int httpCode = http.GET(); Serial.print("HTTP Code : "); Serial.println(httpCode); if (httpCode > 0) { Serial.println(http.getString()); } http.end(); delay(1500); getPredictionFromServer(); } void getPredictionFromServer() { HTTPClient http; http.setTimeout(10000); http.begin("http://10.107.21.239/iot/save_data.php?action=latest"); int httpCode = http.GET(); if (httpCode == 200) { String payload = http.getString(); Serial.println("===== RESPONSE SERVER ====="); Serial.println(payload); JsonDocument doc; deserializeJson(doc, payload); systolic = doc["systolic"] | 120; diastolic = doc["diastolic"] | 80; Serial.print("Systolic dari server : "); Serial.println(systolic); Serial.print("Diastolic dari server : "); Serial.println(diastolic); showFinalResult(); } http.end(); }