pleasedontcode

**Vital Monitor** rev_07

Mar 13th, 2026
41
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Arduino 29.82 KB | None | 0 0
  1. /********* Pleasedontcode.com **********
  2.  
  3.     Pleasedontcode thanks you for automatic code generation! Enjoy your code!
  4.  
  5.     - Terms and Conditions:
  6.     You have a non-exclusive, revocable, worldwide, royalty-free license
  7.     for personal and commercial use. Attribution is optional; modifications
  8.     are allowed, but you're responsible for code maintenance. We're not
  9.     liable for any loss or damage. For full terms,
  10.     please visit pleasedontcode.com/termsandconditions.
  11.  
  12.     - Project: **Vital Monitor**
  13.     - Version: 007
  14.     - Source Code NOT compiled for: ESP32-WROOM-DA
  15.     - Source Code created on: 2026-03-13 13:38:41
  16.  
  17. ********* Pleasedontcode.com **********/
  18.  
  19. /****** SYSTEM REQUIREMENTS *****/
  20. /****** SYSTEM REQUIREMENT 1 *****/
  21.     /* ECG GPIO34@250Hz Pan-Tompkins R-peaks. MAX30102 */
  22.     /* PPG I2C@100Hz peaks. PTT=PPG_peak-R_peak. */
  23.     /* HR=60/RR. SpO2=104-17*R. BP from PTT+offset. OLED: */
  24.     /* HR, SpO2%, BP. Status (ECG/PPG valid). FreeRTOS */
  25.     /* Core0=sensors. Serial CAL only 115200. */
  26. /****** SYSTEM REQUIREMENT 2 *****/
  27.     /* [POTA] Include POTA.h and secrets.h. Init POTA in */
  28.     /* setup() with WiFi creds from secrets.h. Call */
  29.     /* pota.loop() in loop(). */
  30. /****** SYSTEM REQUIREMENT 3 *****/
  31.     /* [POTA] MANDATORY OTA: call */
  32.     /* pota.checkAndPerformOTA() in setup() after */
  33.     /* begin(). Register pota.onOTAAvailable(cb) where cb */
  34.     /* sets a bool flag. In loop() when flag is true call */
  35.     /* pota.restart(). Without OTA the device cannot */
  36.     /* update remotely. */
  37. /****** END SYSTEM REQUIREMENTS *****/
  38.  
  39.  
  40. /* START CODE */
  41.  
  42. /****** DEFINITION OF LIBRARIES *****/
  43. #include <EasyButton.h>      // https://github.com/evert-arias/EasyButton
  44. #include <U8g2lib.h>         // https://github.com/olikraus/u8g2
  45. #include <Wire.h>
  46. #include <freertos/FreeRTOS.h>
  47. #include <freertos/task.h>
  48. #include <freertos/queue.h>
  49. #include "secrets.h"          // WiFi and POTA credentials
  50. #include "POTA.h"             // POTA OTA update library
  51.  
  52. /****** FUNCTION PROTOTYPES *****/
  53. void setup(void);
  54. void loop(void);
  55. void ecgTask(void *pvParameters);
  56. void serialTask(void *pvParameters);
  57. void initializeMAX30102(void);
  58. void initializeOLED(void);
  59. void readECGSignal(void);
  60. void readPPGSignal(void);
  61. void processPanTompkins(void);
  62. void processPPG(void);
  63. void calculateVitals(void);
  64. void displayDashboard(void);
  65. void handleSerialCalibration(void);
  66. void setRGBColor(uint8_t red, uint8_t green, uint8_t blue);
  67. void logDebugInfo(void);
  68. void onOTAAvailableCallback(const char* version);
  69.  
  70. /***** DEFINITION OF DIGITAL INPUT PINS *****/
  71. const uint8_t AD8232_ECG_PushButton_PIN_D4 = 4;
  72. const uint8_t ECG_SIGNAL_PIN = 34;
  73.  
  74. /***** DEFINITION OF DIGITAL OUTPUT PINS *****/
  75. const uint8_t MAX30102_PPG_LED_PIN_D13 = 13;
  76. const uint8_t SH1106_OLED_LEDRGB_Red_PIN_D14 = 14;
  77. // For ESP32-WROOM-DA: GPIO16 and GPIO17 are UART2 pins (replaces RX2/TX2 constants)
  78. const uint8_t SH1106_OLED_LEDRGB_Green_PIN_RX2 = 16;
  79. const uint8_t SH1106_OLED_LEDRGB_Blue_PIN_TX2 = 17;
  80.  
  81. /***** I2C PINS *****/
  82. const uint8_t I2C_SDA = 21;
  83. const uint8_t I2C_SCL = 22;
  84.  
  85. /***** MAX30102 I2C ADDRESS *****/
  86. const uint8_t MAX30102_ADDRESS = 0x57;
  87.  
  88. /***** OLED I2C ADDRESS *****/
  89. const uint8_t OLED_ADDRESS = 0x3C;
  90.  
  91. /***** SAMPLING RATES AND BUFFER SIZES *****/
  92. const uint16_t ECG_SAMPLE_RATE = 250;          // 250 Hz
  93. const uint16_t PPG_SAMPLE_RATE = 100;          // 100 Hz
  94. const uint16_t ECG_BUFFER_SIZE = 250;          // 1 second of ECG data
  95. const uint16_t PPG_BUFFER_SIZE = 100;          // 1 second of PPG data
  96. const uint16_t PAN_TOMPKINS_WINDOW = 250;      // Window for Pan-Tompkins processing
  97.  
  98. /***** PAN-TOMPKINS ALGORITHM CONSTANTS *****/
  99. const float LOW_PASS_FREQ = 5.0;               // Hz
  100. const float HIGH_PASS_FREQ = 8.0;              // Hz
  101. const uint16_t DERIVATIVE_WINDOW = 5;          // Samples
  102. const uint16_t MOVING_WINDOW_INTEGRATION = 13; // Samples
  103. const float THRESHOLD_FACTOR = 0.4;            // Threshold multiplier
  104.  
  105. /***** PPG PROCESSING CONSTANTS *****/
  106. const uint16_t PPG_LED_CURRENT = 18;           // mA (0-255 for MAX30102)
  107. const uint8_t PPG_SPO2_COEFFICIENT = 17;       // For SpO2 calculation
  108. const uint8_t PPG_SPO2_OFFSET = 104;           // For SpO2 calculation
  109.  
  110. /***** BP CALCULATION CONSTANTS (PTT-based) *****/
  111. const float PTT_TO_SBP_A = -0.5;               // Calibration coefficient A
  112. const float PTT_TO_SBP_B = 150.0;              // Calibration coefficient B
  113. const float PTT_TO_DBP_A = -0.3;               // Calibration coefficient A
  114. const float PTT_TO_DBP_B = 90.0;               // Calibration coefficient B
  115.  
  116. /***** VITAL SIGN LIMITS *****/
  117. const uint8_t HR_MIN = 40;
  118. const uint8_t HR_MAX = 200;
  119. const uint8_t SPO2_MIN = 80;
  120. const uint8_t SPO2_MAX = 100;
  121. const uint8_t BP_SBP_NORMAL_MAX = 120;
  122. const uint8_t BP_DBP_NORMAL_MAX = 80;
  123. const uint8_t BP_SBP_ELEVATED_MAX = 129;
  124. const uint8_t BP_SBP_HIGH_MIN = 130;
  125. const uint8_t BP_DBP_HIGH_MIN = 80;
  126.  
  127. /***** STRUCTS FOR DATA *****/
  128. typedef struct {
  129.     uint16_t ecgBuffer[ECG_BUFFER_SIZE];
  130.     uint16_t ppgBuffer[PPG_BUFFER_SIZE];
  131.     uint16_t ecgIndex;
  132.     uint16_t ppgIndex;
  133.     uint32_t ecgSampleCount;
  134.     uint32_t ppgSampleCount;
  135.     uint32_t lastRPeakTime;
  136.     uint32_t lastPPGPeakTime;
  137.     float currentHR;
  138.     float currentSpO2;
  139.     uint8_t currentSBP;
  140.     uint8_t currentDBP;
  141.     bool ecgValid;
  142.     bool ppgValid;
  143.     float calibrationSBP;
  144.     float calibrationDBP;
  145. } HealthData_t;
  146.  
  147. typedef struct {
  148.     float filteredECG[PAN_TOMPKINS_WINDOW];
  149.     float derivativeECG[PAN_TOMPKINS_WINDOW];
  150.     float squaredECG[PAN_TOMPKINS_WINDOW];
  151.     float integratedECG[PAN_TOMPKINS_WINDOW];
  152.     uint16_t filterIndex;
  153.     float lowPassA;
  154.     float lowPassB;
  155.     float highPassA;
  156.     float highPassB;
  157. } PanTompkins_t;
  158.  
  159. typedef struct {
  160.     uint16_t redBuffer[PPG_BUFFER_SIZE];
  161.     uint16_t irBuffer[PPG_BUFFER_SIZE];
  162.     uint8_t ppgIndex;
  163.     float rValue;
  164.     uint8_t lastPPGPeakDetected;
  165. } PPGData_t;
  166.  
  167. /***** OLED DISPLAY OBJECT *****/
  168. U8G2_SH1106_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE, I2C_SCL, I2C_SDA);
  169.  
  170. /***** GLOBAL VARIABLES *****/
  171. HealthData_t healthData;
  172. PanTompkins_t panTompkins;
  173. PPGData_t ppgData;
  174. EasyButton button(AD8232_ECG_PushButton_PIN_D4);
  175. QueueHandle_t dataQueue;
  176. TaskHandle_t ecgTaskHandle = NULL;
  177. TaskHandle_t serialTaskHandle = NULL;
  178. volatile bool systemRunning = true;
  179.  
  180. // POTA OTA update management
  181. POTA pota;
  182. volatile bool otaAvailable = false;
  183.  
  184. /***** TIMER VARIABLES *****/
  185. hw_timer_t *ecgTimer = NULL;
  186. hw_timer_t *ppgTimer = NULL;
  187. volatile uint32_t ecgTimerCount = 0;
  188. volatile uint32_t ppgTimerCount = 0;
  189.  
  190. /****** INITIALIZATION FUNCTION *****/
  191. void initializeSystemVariables(void) {
  192.     // Initialize health data structure
  193.     healthData.ecgIndex = 0;
  194.     healthData.ppgIndex = 0;
  195.     healthData.ecgSampleCount = 0;
  196.     healthData.ppgSampleCount = 0;
  197.     healthData.lastRPeakTime = 0;
  198.     healthData.lastPPGPeakTime = 0;
  199.     healthData.currentHR = 0.0;
  200.     healthData.currentSpO2 = 95.0;
  201.     healthData.currentSBP = 120;
  202.     healthData.currentDBP = 80;
  203.     healthData.ecgValid = false;
  204.     healthData.ppgValid = false;
  205.     healthData.calibrationSBP = 120.0;
  206.     healthData.calibrationDBP = 80.0;
  207.    
  208.     memset(healthData.ecgBuffer, 0, sizeof(healthData.ecgBuffer));
  209.     memset(healthData.ppgBuffer, 0, sizeof(healthData.ppgBuffer));
  210.    
  211.     // Initialize Pan-Tompkins filter coefficients
  212.     float wc = 2.0 * PI * LOW_PASS_FREQ / ECG_SAMPLE_RATE;
  213.     panTompkins.lowPassA = wc / (2.0 + wc);
  214.     panTompkins.lowPassB = wc / (2.0 + wc);
  215.    
  216.     wc = 2.0 * PI * HIGH_PASS_FREQ / ECG_SAMPLE_RATE;
  217.     panTompkins.highPassA = 2.0 / (2.0 + wc);
  218.     panTompkins.highPassB = (2.0 - wc) / (2.0 + wc);
  219.    
  220.     panTompkins.filterIndex = 0;
  221.     memset(panTompkins.filteredECG, 0, sizeof(panTompkins.filteredECG));
  222.     memset(panTompkins.derivativeECG, 0, sizeof(panTompkins.derivativeECG));
  223.     memset(panTompkins.squaredECG, 0, sizeof(panTompkins.squaredECG));
  224.     memset(panTompkins.integratedECG, 0, sizeof(panTompkins.integratedECG));
  225.    
  226.     // Initialize PPG data structure
  227.     ppgData.ppgIndex = 0;
  228.     ppgData.rValue = 1.0;
  229.     ppgData.lastPPGPeakDetected = 0;
  230.     memset(ppgData.redBuffer, 0, sizeof(ppgData.redBuffer));
  231.     memset(ppgData.irBuffer, 0, sizeof(ppgData.irBuffer));
  232. }
  233.  
  234. /****** ECG TIMER INTERRUPT *****/
  235. void IRAM_ATTR ecgTimerISR(void) {
  236.     ecgTimerCount++;
  237.     if (ecgTimerCount % (80000000 / ECG_SAMPLE_RATE) == 0) {
  238.         readECGSignal();
  239.     }
  240. }
  241.  
  242. /****** PPG TIMER INTERRUPT *****/
  243. void IRAM_ATTR ppgTimerISR(void) {
  244.     ppgTimerCount++;
  245.     if (ppgTimerCount % (80000000 / PPG_SAMPLE_RATE) == 0) {
  246.         readPPGSignal();
  247.     }
  248. }
  249.  
  250. /****** POTA OTA AVAILABLE CALLBACK - Called when OTA update is available *****/
  251. void onOTAAvailableCallback(const char* version) {
  252.     // Called when POTA detects a new firmware update is available
  253.     // The version parameter contains the available firmware version
  254.     Serial.print("[POTA] OTA update available! Version: ");
  255.     Serial.println(version);
  256.     Serial.println("[POTA] Flag set for restart.");\n    otaAvailable = true;
  257. }
  258.  
  259. /****** SETUP FUNCTION *****/
  260. void setup(void) {
  261.     // Initialize serial communication at 115200 baud
  262.     Serial.begin(115200);
  263.     delay(500);
  264.     Serial.println("\n\n========== ECG+PPG Health Monitor Initializing ==========");
  265.    
  266.     // Initialize pins
  267.     pinMode(AD8232_ECG_PushButton_PIN_D4, INPUT_PULLUP);
  268.     pinMode(MAX30102_PPG_LED_PIN_D13, OUTPUT);
  269.     pinMode(SH1106_OLED_LEDRGB_Red_PIN_D14, OUTPUT);
  270.     pinMode(SH1106_OLED_LEDRGB_Green_PIN_RX2, OUTPUT);
  271.     pinMode(SH1106_OLED_LEDRGB_Blue_PIN_TX2, OUTPUT);
  272.    
  273.     // Initialize I2C
  274.     Wire.begin(I2C_SDA, I2C_SCL);
  275.     Wire.setClock(400000);
  276.     Serial.println("[INIT] I2C initialized at 400kHz");
  277.    
  278.     // Initialize system variables
  279.     initializeSystemVariables();
  280.     Serial.println("[INIT] System variables initialized");
  281.    
  282.     // Initialize OLED display
  283.     initializeOLED();
  284.     Serial.println("[INIT] OLED display initialized");
  285.    
  286.     // Initialize MAX30102 sensor
  287.     initializeMAX30102();
  288.     Serial.println("[INIT] MAX30102 PPG sensor initialized");
  289.    
  290.     // Initialize button
  291.     button.begin();
  292.     Serial.println("[INIT] EasyButton initialized");
  293.    
  294.     // Initialize POTA OTA update system with WiFi credentials from secrets.h
  295.     Serial.println("[POTA] Initializing OTA update system...");
  296.     pota.begin(DEVICE_TYPE, FIRMWARE_VERSION, AUTH_TOKEN, SERVER_SECRET, WIFI_SSID, WIFI_PASSWORD);
  297.     Serial.println("[POTA] POTA initialized with WiFi credentials from secrets.h");
  298.    
  299.     // Register OTA available callback - callback accepts const char* version parameter
  300.     pota.onOTAAvailable(onOTAAvailableCallback);
  301.     Serial.println("[POTA] OTA callback registered");
  302.    
  303.     // Check for OTA updates immediately after initialization (MANDATORY)
  304.     Serial.println("[POTA] Checking for available OTA updates...");
  305.     pota.checkAndPerformOTA();
  306.     Serial.println("[POTA] OTA check completed");
  307.    
  308.     // Create data queue for inter-task communication
  309.     dataQueue = xQueueCreate(10, sizeof(HealthData_t));
  310.     Serial.println("[INIT] Message queue created");
  311.    
  312.     // Create FreeRTOS tasks
  313.     xTaskCreatePinnedToCore(
  314.         ecgTask,
  315.         "ECG_TASK",
  316.         4096,
  317.         NULL,
  318.         3,
  319.         &ecgTaskHandle,
  320.         0  // Core 0: Sensor acquisition and UI updates
  321.     );
  322.    
  323.     xTaskCreatePinnedToCore(
  324.         serialTask,
  325.         "SERIAL_TASK",
  326.         2048,
  327.         NULL,
  328.         2,
  329.         &serialTaskHandle,
  330.         1  // Core 1: Debug serial output
  331.     );
  332.    
  333.     Serial.println("[INIT] FreeRTOS tasks created");
  334.     Serial.println("========== System Ready ==========\n");
  335.     Serial.println("Serial Calibration Command: CAL SBP:120 DBP:80");
  336.     Serial.println("(Replace values with measured SBP and DBP)\n");
  337.    
  338.     // Set initial RGB color (green)
  339.     setRGBColor(0, 255, 0);
  340. }
  341.  
  342. /****** MAIN LOOP (RUNS ON CORE 1) *****/
  343. void loop(void) {
  344.     // Call POTA loop to handle background OTA operations
  345.     pota.loop();
  346.    
  347.     // Check if OTA update is available and needs restart
  348.     if (otaAvailable) {
  349.         Serial.println("[POTA] OTA update available - initiating device restart for update...");
  350.         setRGBColor(255, 0, 255);  // Magenta: OTA in progress
  351.         delay(500);
  352.         // Call POTA restart to reboot into OTA mode
  353.         pota.restart();
  354.         // This will not return - device reboots
  355.     }
  356.    
  357.     vTaskDelay(1000 / portTICK_PERIOD_MS);
  358. }
  359.  
  360. /****** ECG AND SENSOR ACQUISITION TASK (CORE 0) *****/
  361. void ecgTask(void *pvParameters) {
  362.     TickType_t xLastWakeTime = xTaskGetTickCount();
  363.     const TickType_t xFrequency = pdMS_TO_TICKS(10);
  364.    
  365.     Serial.println("[TASK] ECG Task started on Core 0");
  366.    
  367.     while (systemRunning) {
  368.         // Handle button events
  369.         button.read();
  370.        
  371.         // Process ECG data when buffer is half full
  372.         if (healthData.ecgIndex >= ECG_BUFFER_SIZE / 2) {
  373.             processPanTompkins();
  374.         }
  375.        
  376.         // Process PPG data when buffer is half full
  377.         if (healthData.ppgIndex >= PPG_BUFFER_SIZE / 2) {
  378.             processPPG();
  379.         }
  380.        
  381.         // Calculate vitals
  382.         if (healthData.ecgValid && healthData.ppgValid) {
  383.             calculateVitals();
  384.         }
  385.        
  386.         // Update OLED display
  387.         displayDashboard();
  388.        
  389.         xTaskDelayUntil(&xLastWakeTime, xFrequency);
  390.     }
  391.    
  392.     vTaskDelete(NULL);
  393. }
  394.  
  395. /****** SERIAL DEBUG AND CALIBRATION TASK (CORE 1) *****/
  396. void serialTask(void *pvParameters) {
  397.     Serial.println("[TASK] Serial Task started on Core 1");
  398.    
  399.     while (systemRunning) {
  400.         // Handle serial calibration commands
  401.         if (Serial.available()) {
  402.             handleSerialCalibration();
  403.         }
  404.        
  405.         // Output debug info periodically
  406.         static uint32_t lastDebugTime = 0;
  407.         if (millis() - lastDebugTime > 5000) {
  408.             logDebugInfo();
  409.             lastDebugTime = millis();
  410.         }
  411.        
  412.         vTaskDelay(100 / portTICK_PERIOD_MS);
  413.     }
  414.    
  415.     vTaskDelete(NULL);
  416. }
  417.  
  418. /****** OLED INITIALIZATION *****/
  419. void initializeOLED(void) {
  420.     u8g2.begin();
  421.     u8g2.setFont(u8g2_font_ncenB10_tr);
  422.     u8g2.clearBuffer();
  423.     u8g2.drawStr(10, 32, "Initializing...");
  424.     u8g2.sendBuffer();
  425.     Serial.println("[OLED] Display initialized at I2C 0x3C");
  426. }
  427.  
  428. /****** MAX30102 INITIALIZATION *****/
  429. void initializeMAX30102(void) {
  430.     // Reset MAX30102
  431.     Wire.beginTransmission(MAX30102_ADDRESS);
  432.     Wire.write(0x09);  // Mode Configuration Register
  433.     Wire.write(0x40);  // Reset bit
  434.     Wire.endTransmission();
  435.     delay(100);
  436.    
  437.     // Configure for dual mode (HR + SpO2)
  438.     Wire.beginTransmission(MAX30102_ADDRESS);
  439.     Wire.write(0x09);  // Mode Configuration Register
  440.     Wire.write(0x03);  // SpO2 mode
  441.     Wire.endTransmission();
  442.    
  443.     // Set LED currents
  444.     Wire.beginTransmission(MAX30102_ADDRESS);
  445.     Wire.write(0x0C);  // LED1 Pulse Amplitude (RED)
  446.     Wire.write(PPG_LED_CURRENT);
  447.     Wire.endTransmission();
  448.    
  449.     Wire.beginTransmission(MAX30102_ADDRESS);
  450.     Wire.write(0x0D);  // LED2 Pulse Amplitude (IR)
  451.     Wire.write(PPG_LED_CURRENT);
  452.     Wire.endTransmission();
  453.    
  454.     // Set sample rate to 100Hz
  455.     Wire.beginTransmission(MAX30102_ADDRESS);
  456.     Wire.write(0x08);  // FIFO Configuration
  457.     Wire.write(0x0F);  // Sample averaging and FIFO rollover
  458.     Wire.endTransmission();
  459.    
  460.     // Clear FIFO pointers
  461.     Wire.beginTransmission(MAX30102_ADDRESS);
  462.     Wire.write(0x04);  // FIFO Write Pointer
  463.     Wire.write(0x00);
  464.     Wire.endTransmission();
  465.    
  466.     Wire.beginTransmission(MAX30102_ADDRESS);
  467.     Wire.write(0x06);  // FIFO Read Pointer
  468.     Wire.write(0x00);
  469.     Wire.endTransmission();
  470.    
  471.     delay(100);
  472.     Serial.println("[MAX30102] PPG sensor configured");
  473. }
  474.  
  475. /****** READ ECG SIGNAL FROM GPIO34 *****/
  476. void readECGSignal(void) {
  477.     // Read analog ECG signal from GPIO34
  478.     uint16_t ecgRaw = analogRead(ECG_SIGNAL_PIN);
  479.    
  480.     // Validate ECG signal (should be in reasonable range, not saturated)
  481.     if (ecgRaw > 200 && ecgRaw < 3900) {
  482.         healthData.ecgValid = true;
  483.     } else if (ecgRaw <= 200 || ecgRaw >= 3900) {
  484.         healthData.ecgValid = false;
  485.     }
  486.    
  487.     // Store in circular buffer
  488.     healthData.ecgBuffer[healthData.ecgIndex] = ecgRaw;
  489.     healthData.ecgIndex = (healthData.ecgIndex + 1) % ECG_BUFFER_SIZE;
  490.     healthData.ecgSampleCount++;
  491. }
  492.  
  493. /****** READ PPG SIGNAL FROM MAX30102 *****/
  494. void readPPGSignal(void) {
  495.     uint8_t readPtr, writePtr, numSamples;
  496.    
  497.     // Read FIFO pointers
  498.     Wire.beginTransmission(MAX30102_ADDRESS);
  499.     Wire.write(0x04);  // Write Pointer
  500.     Wire.endTransmission();
  501.     Wire.requestFrom(MAX30102_ADDRESS, (uint8_t)1);
  502.     writePtr = Wire.read();
  503.    
  504.     Wire.beginTransmission(MAX30102_ADDRESS);
  505.     Wire.write(0x06);  // Read Pointer
  506.     Wire.endTransmission();
  507.     Wire.requestFrom(MAX30102_ADDRESS, (uint8_t)1);
  508.     readPtr = Wire.read();
  509.    
  510.     numSamples = (writePtr - readPtr) & 0x1F;
  511.    
  512.     // Read FIFO data
  513.     for (uint8_t i = 0; i < numSamples; i++) {
  514.         Wire.beginTransmission(MAX30102_ADDRESS);
  515.         Wire.write(0x07);  // FIFO Data Register
  516.         Wire.endTransmission();
  517.         Wire.requestFrom(MAX30102_ADDRESS, (uint8_t)6);
  518.        
  519.         uint32_t redData = (Wire.read() << 16) | (Wire.read() << 8) | Wire.read();
  520.         uint32_t irData = (Wire.read() << 16) | (Wire.read() << 8) | Wire.read();
  521.        
  522.         redData &= 0x03FFFF;  // 18-bit mask
  523.         irData &= 0x03FFFF;   // 18-bit mask
  524.        
  525.         // Validate PPG signal
  526.         if (irData > 5000 && irData < 200000) {
  527.             healthData.ppgValid = true;
  528.         } else {
  529.             healthData.ppgValid = false;
  530.         }
  531.        
  532.         // Store in circular buffer
  533.         ppgData.redBuffer[healthData.ppgIndex] = redData >> 8;
  534.         ppgData.irBuffer[healthData.ppgIndex] = irData >> 8;
  535.         healthData.ppgIndex = (healthData.ppgIndex + 1) % PPG_BUFFER_SIZE;
  536.         healthData.ppgSampleCount++;
  537.     }
  538. }
  539.  
  540. /****** PAN-TOMPKINS ECG PROCESSING *****/
  541. void processPanTompkins(void) {
  542.     // Apply low-pass filter (cutoff ~5 Hz)
  543.     float filteredValue;
  544.     static float prevFiltered = 0;
  545.    
  546.     for (uint16_t i = 0; i < ECG_BUFFER_SIZE / 2; i++) {
  547.         uint16_t idx = (healthData.ecgIndex + i) % ECG_BUFFER_SIZE;
  548.         float ecgValue = (float)healthData.ecgBuffer[idx] / 4095.0 * 3.3;
  549.        
  550.         // Butterworth low-pass filter
  551.         filteredValue = panTompkins.lowPassA * ecgValue +
  552.                        (1.0 - panTompkins.lowPassA) * prevFiltered;
  553.         prevFiltered = filteredValue;
  554.        
  555.         panTompkins.filteredECG[panTompkins.filterIndex] = filteredValue;
  556.        
  557.         // Apply high-pass filter
  558.         static float prevHighPass = 0;
  559.         float highPassed = panTompkins.highPassA * (filteredValue - prevHighPass) +
  560.                           panTompkins.highPassB * prevHighPass;
  561.         prevHighPass = filteredValue;
  562.        
  563.         // Derivative (5-point difference)
  564.         if (panTompkins.filterIndex >= DERIVATIVE_WINDOW) {
  565.             panTompkins.derivativeECG[panTompkins.filterIndex] =
  566.                 (highPassed - panTompkins.filteredECG[panTompkins.filterIndex - DERIVATIVE_WINDOW]) /
  567.                 (DERIVATIVE_WINDOW * (1.0 / ECG_SAMPLE_RATE));
  568.         }
  569.        
  570.         // Squaring
  571.         panTompkins.squaredECG[panTompkins.filterIndex] =
  572.             panTompkins.derivativeECG[panTompkins.filterIndex] *
  573.             panTompkins.derivativeECG[panTompkins.filterIndex];
  574.        
  575.         // Moving window integration
  576.         if (panTompkins.filterIndex >= MOVING_WINDOW_INTEGRATION) {
  577.             float sum = 0;
  578.             for (uint8_t j = 0; j < MOVING_WINDOW_INTEGRATION; j++) {
  579.                 uint16_t idx = (panTompkins.filterIndex - j) % PAN_TOMPKINS_WINDOW;
  580.                 sum += panTompkins.squaredECG[idx];
  581.             }
  582.             panTompkins.integratedECG[panTompkins.filterIndex] =
  583.                 sum / MOVING_WINDOW_INTEGRATION;
  584.            
  585.             // R-peak detection with threshold
  586.             static float threshold = 0.01;
  587.             if (panTompkins.integratedECG[panTompkins.filterIndex] > threshold) {
  588.                 // Check if this is a local maximum (avoid duplicate detections)
  589.                 if (panTompkins.filterIndex > 0 &&
  590.                     panTompkins.integratedECG[panTompkins.filterIndex] >
  591.                     panTompkins.integratedECG[(panTompkins.filterIndex - 1) % PAN_TOMPKINS_WINDOW]) {
  592.                    
  593.                     uint32_t currentTime = millis();
  594.                    
  595.                     // RR interval validation (HR between 40-200 bpm = 300-1500ms)
  596.                     if (healthData.lastRPeakTime > 0) {
  597.                         uint32_t rrInterval = currentTime - healthData.lastRPeakTime;
  598.                         if (rrInterval >= 300 && rrInterval <= 1500) {
  599.                             healthData.lastRPeakTime = currentTime;
  600.                         }
  601.                     } else {
  602.                         healthData.lastRPeakTime = currentTime;
  603.                     }
  604.                    
  605.                     // Update adaptive threshold
  606.                     threshold = THRESHOLD_FACTOR * panTompkins.integratedECG[panTompkins.filterIndex];
  607.                 }
  608.             }
  609.         }
  610.        
  611.         panTompkins.filterIndex = (panTompkins.filterIndex + 1) % PAN_TOMPKINS_WINDOW;
  612.     }
  613. }
  614.  
  615. /****** PPG SIGNAL PROCESSING *****/
  616. void processPPG(void) {
  617.     // Calculate SpO2 ratio (R = (AC_red/DC_red) / (AC_ir/DC_ir))
  618.     float dcRed = 0, dcIR = 0, acRed = 0, acIR = 0;
  619.     float meanRed = 0, meanIR = 0;
  620.    
  621.     // Calculate mean (DC components)
  622.     for (uint16_t i = 0; i < PPG_BUFFER_SIZE; i++) {
  623.         meanRed += ppgData.redBuffer[i];
  624.         meanIR += ppgData.irBuffer[i];
  625.     }
  626.     meanRed /= PPG_BUFFER_SIZE;
  627.     meanIR /= PPG_BUFFER_SIZE;
  628.    
  629.     dcRed = meanRed;
  630.     dcIR = meanIR;
  631.    
  632.     // Calculate AC components (peak-to-peak)
  633.     float maxRed = 0, minRed = 65535;
  634.     float maxIR = 0, minIR = 65535;
  635.    
  636.     for (uint16_t i = 0; i < PPG_BUFFER_SIZE; i++) {
  637.         if (ppgData.redBuffer[i] > maxRed) maxRed = ppgData.redBuffer[i];
  638.         if (ppgData.redBuffer[i] < minRed) minRed = ppgData.redBuffer[i];
  639.         if (ppgData.irBuffer[i] > maxIR) maxIR = ppgData.irBuffer[i];
  640.         if (ppgData.irBuffer[i] < minIR) minIR = ppgData.irBuffer[i];
  641.     }
  642.    
  643.     acRed = maxRed - minRed;
  644.     acIR = maxIR - minIR;
  645.    
  646.     // Prevent division by zero
  647.     if (dcRed > 0 && dcIR > 0 && acRed > 0 && acIR > 0) {
  648.         ppgData.rValue = (acRed / dcRed) / (acIR / dcIR);
  649.        
  650.         // Calculate SpO2 using empirical formula
  651.         // SpO2 = 104 - 17*R
  652.         float calculatedSpO2 = PPG_SPO2_OFFSET - PPG_SPO2_COEFFICIENT * ppgData.rValue;
  653.        
  654.         // Validate SpO2 range
  655.         if (calculatedSpO2 >= SPO2_MIN && calculatedSpO2 <= SPO2_MAX) {
  656.             healthData.currentSpO2 = calculatedSpO2;
  657.         } else if (calculatedSpO2 < SPO2_MIN) {
  658.             healthData.currentSpO2 = SPO2_MIN;
  659.         } else {
  660.             healthData.currentSpO2 = SPO2_MAX;
  661.         }
  662.        
  663.         // Detect PPG peak
  664.         if (maxIR > meanIR + (acIR / 2)) {
  665.             healthData.lastPPGPeakTime = millis();
  666.             ppgData.lastPPGPeakDetected = 1;
  667.         }
  668.     }
  669. }
  670.  
  671. /****** CALCULATE VITAL SIGNS *****/
  672. void calculateVitals(void) {
  673.     // Calculate Heart Rate from R-R interval
  674.     if (healthData.lastRPeakTime > 0) {
  675.         uint32_t currentTime = millis();
  676.         uint32_t rrInterval = currentTime - healthData.lastRPeakTime;
  677.        
  678.         if (rrInterval > 0) {
  679.             healthData.currentHR = 60000.0 / rrInterval;
  680.            
  681.             // Validate HR range
  682.             if (healthData.currentHR < HR_MIN || healthData.currentHR > HR_MAX) {
  683.                 healthData.currentHR = 0;
  684.             }
  685.         }
  686.     }
  687.    
  688.     // Calculate Blood Pressure from PTT
  689.     if (healthData.lastRPeakTime > 0 && healthData.lastPPGPeakTime > 0) {
  690.         int32_t ptt = (int32_t)healthData.lastPPGPeakTime - (int32_t)healthData.lastRPeakTime;
  691.        
  692.         // PTT should be positive (PPG peak after R-peak)
  693.         if (ptt > 0 && ptt < 1000) {
  694.             // Convert PTT (ms) to PTT-based BP with calibration
  695.             healthData.currentSBP = (uint8_t)constrain(
  696.                 (PTT_TO_SBP_A * ptt) + PTT_TO_SBP_B + healthData.calibrationSBP,
  697.                 60, 200
  698.             );
  699.            
  700.             healthData.currentDBP = (uint8_t)constrain(
  701.                 (PTT_TO_DBP_A * ptt) + PTT_TO_DBP_B + healthData.calibrationDBP,
  702.                 40, 120
  703.             );
  704.         }
  705.     }
  706. }
  707.  
  708. /****** DISPLAY CLEAN DASHBOARD ON OLED (NO CALIBRATION UI) *****/
  709. void displayDashboard(void) {
  710.     static uint32_t lastDisplayTime = 0;
  711.     uint32_t currentTime = millis();
  712.    
  713.     // Update display every 500ms to reduce flicker
  714.     if (currentTime - lastDisplayTime < 500) {
  715.         return;
  716.     }
  717.     lastDisplayTime = currentTime;
  718.    
  719.     u8g2.clearBuffer();
  720.    
  721.     // Title line (small font)
  722.     u8g2.setFont(u8g2_font_ncenB08_tr);
  723.     u8g2.drawStr(0, 10, "Vital Signs");
  724.    
  725.     // Separator line to prevent overlap
  726.     u8g2.drawHLine(0, 12, 128);
  727.    
  728.     // HR line (Heart Rate in bpm) - large font
  729.     u8g2.setFont(u8g2_font_ncenB14_tr);
  730.     char hrStr[10];
  731.     snprintf(hrStr, sizeof(hrStr), "%3.0f", healthData.currentHR);
  732.     u8g2.drawStr(5, 32, hrStr);
  733.    
  734.     u8g2.setFont(u8g2_font_ncenB10_tr);
  735.     u8g2.drawStr(50, 32, "bpm");
  736.    
  737.     // SpO2 line (Oxygen Saturation) - large font
  738.     u8g2.setFont(u8g2_font_ncenB14_tr);
  739.     char spo2Str[8];
  740.     snprintf(spo2Str, sizeof(spo2Str), "%3.0f%%", healthData.currentSpO2);
  741.     u8g2.drawStr(5, 48, spo2Str);
  742.    
  743.     // BP line (Blood Pressure) - medium font
  744.     u8g2.setFont(u8g2_font_ncenB12_tr);
  745.     char bpStr[12];
  746.     snprintf(bpStr, sizeof(bpStr), "%3d/%2d", healthData.currentSBP, healthData.currentDBP);
  747.     u8g2.drawStr(5, 62, bpStr);
  748.    
  749.     // Status indicators on the right side (small font, no overlap)
  750.     u8g2.setFont(u8g2_font_ncenB08_tr);
  751.     u8g2.drawStr(100, 32, healthData.ecgValid ? "ECG" : "---");
  752.     u8g2.drawStr(100, 48, healthData.ppgValid ? "PPG" : "---");
  753.    
  754.     // Send to display
  755.     u8g2.sendBuffer();
  756. }
  757.  
  758. /****** HANDLE SERIAL CALIBRATION (CAL COMMAND ONLY) *****/
  759. void handleSerialCalibration(void) {
  760.     static String calibrationBuffer = "";
  761.    
  762.     while (Serial.available()) {
  763.         char inChar = Serial.read();
  764.         Serial.write(inChar);  // Echo the input
  765.        
  766.         if (inChar == '\n' || inChar == '\r') {
  767.             if (calibrationBuffer.length() > 0) {
  768.                 calibrationBuffer.toUpperCase();
  769.                
  770.                 // Parse calibration command: CAL SBP:XXX DBP:YYY
  771.                 if (calibrationBuffer.startsWith("CAL")) {
  772.                     // Find SBP and DBP indices
  773.                     int sbpIndex = calibrationBuffer.indexOf("SBP:");
  774.                     int dbpIndex = calibrationBuffer.indexOf("DBP:");
  775.                    
  776.                     if (sbpIndex >= 0 && dbpIndex >= 0) {
  777.                         // Extract SBP value
  778.                         String sbpStr = calibrationBuffer.substring(sbpIndex + 4);
  779.                         sbpStr = sbpStr.substring(0, sbpStr.indexOf(" "));
  780.                        
  781.                         // Extract DBP value
  782.                         String dbpStr = calibrationBuffer.substring(dbpIndex + 4);
  783.                        
  784.                         int calibSBP = sbpStr.toInt();
  785.                         int calibDBP = dbpStr.toInt();
  786.                        
  787.                         // Validate ranges (realistic BP values)
  788.                         if (calibSBP > 60 && calibSBP < 200 && calibDBP > 40 && calibDBP < 120) {
  789.                             healthData.calibrationSBP = calibSBP;
  790.                             healthData.calibrationDBP = calibDBP;
  791.                             Serial.println("");
  792.                             Serial.print("[CALIB] SUCCESS: SBP=");
  793.                             Serial.print(calibSBP);
  794.                             Serial.print(", DBP=");
  795.                             Serial.println(calibDBP);
  796.                         } else {
  797.                             Serial.println("");
  798.                             Serial.println("[CALIB] ERROR: Invalid range. Use 60<SBP<200, 40<DBP<120");
  799.                         }
  800.                     } else {
  801.                         Serial.println("");
  802.                         Serial.println("[CALIB] ERROR: Format: CAL SBP:120 DBP:80");
  803.                     }
  804.                 }
  805.                 calibrationBuffer = "";
  806.             }
  807.         } else {
  808.             calibrationBuffer += inChar;
  809.         }
  810.     }
  811. }
  812.  
  813. /****** SET RGB COLOR (STATUS INDICATOR) *****/
  814. void setRGBColor(uint8_t red, uint8_t green, uint8_t blue) {
  815.     analogWrite(SH1106_OLED_LEDRGB_Red_PIN_D14, red);
  816.     analogWrite(SH1106_OLED_LEDRGB_Green_PIN_RX2, green);
  817.     analogWrite(SH1106_OLED_LEDRGB_Blue_PIN_TX2, blue);
  818. }
  819.  
  820. /****** LOG DEBUG INFORMATION (SERIAL OUTPUT) *****/
  821. void logDebugInfo(void) {
  822.     Serial.println("\n========== SYSTEM STATUS ==========");
  823.     Serial.print("[TIME] Uptime: ");
  824.     Serial.print(millis() / 1000);
  825.     Serial.println(" seconds");
  826.    
  827.     Serial.print("[ECG] Samples: ");
  828.     Serial.print(healthData.ecgSampleCount);
  829.     Serial.print(" | Valid: ");
  830.     Serial.println(healthData.ecgValid ? "YES" : "NO");
  831.    
  832.     Serial.print("[PPG] Samples: ");
  833.     Serial.print(healthData.ppgSampleCount);
  834.     Serial.print(" | Valid: ");
  835.     Serial.println(healthData.ppgValid ? "YES" : "NO");
  836.    
  837.     Serial.print("[VITALS] HR: ");
  838.     Serial.print(healthData.currentHR, 1);
  839.     Serial.print(" bpm | SpO2: ");
  840.     Serial.print(healthData.currentSpO2, 1);
  841.     Serial.println(" %");
  842.    
  843.     Serial.print("[BP] SBP: ");
  844.     Serial.print(healthData.currentSBP);
  845.     Serial.print(" mmHg | DBP: ");
  846.     Serial.print(healthData.currentDBP);
  847.     Serial.println(" mmHg");
  848.    
  849.     Serial.print("[CALIB] SBP: ");
  850.     Serial.print(healthData.calibrationSBP, 1);
  851.     Serial.print(" | DBP: ");
  852.     Serial.print(healthData.calibrationDBP, 1);
  853.     Serial.println("");
  854.    
  855.     Serial.println("====================================\n");
  856.    
  857.     // Update RGB LED based on BP status for visual feedback
  858.     if (healthData.currentSBP <= BP_SBP_NORMAL_MAX &&
  859.         healthData.currentDBP <= BP_DBP_NORMAL_MAX) {
  860.         setRGBColor(0, 255, 0);  // Green - Normal BP
  861.     } else if (healthData.currentSBP <= BP_SBP_ELEVATED_MAX) {
  862.         setRGBColor(255, 255, 0);  // Yellow - Elevated BP
  863.     } else {
  864.         setRGBColor(255, 0, 0);  // Red - High BP
  865.     }
  866. }
  867.  
  868. /* END CODE */
  869.  
Advertisement
Add Comment
Please, Sign In to add comment