Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- /********* Pleasedontcode.com **********
- Pleasedontcode thanks you for automatic code generation! Enjoy your code!
- - Terms and Conditions:
- You have a non-exclusive, revocable, worldwide, royalty-free license
- for personal and commercial use. Attribution is optional; modifications
- are allowed, but you're responsible for code maintenance. We're not
- liable for any loss or damage. For full terms,
- please visit pleasedontcode.com/termsandconditions.
- - Project: **Vital Monitor**
- - Version: 007
- - Source Code NOT compiled for: ESP32-WROOM-DA
- - Source Code created on: 2026-03-13 13:38:41
- ********* Pleasedontcode.com **********/
- /****** SYSTEM REQUIREMENTS *****/
- /****** SYSTEM REQUIREMENT 1 *****/
- /* ECG GPIO34@250Hz Pan-Tompkins R-peaks. MAX30102 */
- /* PPG I2C@100Hz peaks. PTT=PPG_peak-R_peak. */
- /* HR=60/RR. SpO2=104-17*R. BP from PTT+offset. OLED: */
- /* HR, SpO2%, BP. Status (ECG/PPG valid). FreeRTOS */
- /* Core0=sensors. Serial CAL only 115200. */
- /****** SYSTEM REQUIREMENT 2 *****/
- /* [POTA] Include POTA.h and secrets.h. Init POTA in */
- /* setup() with WiFi creds from secrets.h. Call */
- /* pota.loop() in loop(). */
- /****** SYSTEM REQUIREMENT 3 *****/
- /* [POTA] MANDATORY OTA: call */
- /* pota.checkAndPerformOTA() in setup() after */
- /* begin(). Register pota.onOTAAvailable(cb) where cb */
- /* sets a bool flag. In loop() when flag is true call */
- /* pota.restart(). Without OTA the device cannot */
- /* update remotely. */
- /****** END SYSTEM REQUIREMENTS *****/
- /* START CODE */
- /****** DEFINITION OF LIBRARIES *****/
- #include <EasyButton.h> // https://github.com/evert-arias/EasyButton
- #include <U8g2lib.h> // https://github.com/olikraus/u8g2
- #include <Wire.h>
- #include <freertos/FreeRTOS.h>
- #include <freertos/task.h>
- #include <freertos/queue.h>
- #include "secrets.h" // WiFi and POTA credentials
- #include "POTA.h" // POTA OTA update library
- /****** FUNCTION PROTOTYPES *****/
- void setup(void);
- void loop(void);
- void ecgTask(void *pvParameters);
- void serialTask(void *pvParameters);
- void initializeMAX30102(void);
- void initializeOLED(void);
- void readECGSignal(void);
- void readPPGSignal(void);
- void processPanTompkins(void);
- void processPPG(void);
- void calculateVitals(void);
- void displayDashboard(void);
- void handleSerialCalibration(void);
- void setRGBColor(uint8_t red, uint8_t green, uint8_t blue);
- void logDebugInfo(void);
- void onOTAAvailableCallback(const char* version);
- /***** DEFINITION OF DIGITAL INPUT PINS *****/
- const uint8_t AD8232_ECG_PushButton_PIN_D4 = 4;
- const uint8_t ECG_SIGNAL_PIN = 34;
- /***** DEFINITION OF DIGITAL OUTPUT PINS *****/
- const uint8_t MAX30102_PPG_LED_PIN_D13 = 13;
- const uint8_t SH1106_OLED_LEDRGB_Red_PIN_D14 = 14;
- // For ESP32-WROOM-DA: GPIO16 and GPIO17 are UART2 pins (replaces RX2/TX2 constants)
- const uint8_t SH1106_OLED_LEDRGB_Green_PIN_RX2 = 16;
- const uint8_t SH1106_OLED_LEDRGB_Blue_PIN_TX2 = 17;
- /***** I2C PINS *****/
- const uint8_t I2C_SDA = 21;
- const uint8_t I2C_SCL = 22;
- /***** MAX30102 I2C ADDRESS *****/
- const uint8_t MAX30102_ADDRESS = 0x57;
- /***** OLED I2C ADDRESS *****/
- const uint8_t OLED_ADDRESS = 0x3C;
- /***** SAMPLING RATES AND BUFFER SIZES *****/
- const uint16_t ECG_SAMPLE_RATE = 250; // 250 Hz
- const uint16_t PPG_SAMPLE_RATE = 100; // 100 Hz
- const uint16_t ECG_BUFFER_SIZE = 250; // 1 second of ECG data
- const uint16_t PPG_BUFFER_SIZE = 100; // 1 second of PPG data
- const uint16_t PAN_TOMPKINS_WINDOW = 250; // Window for Pan-Tompkins processing
- /***** PAN-TOMPKINS ALGORITHM CONSTANTS *****/
- const float LOW_PASS_FREQ = 5.0; // Hz
- const float HIGH_PASS_FREQ = 8.0; // Hz
- const uint16_t DERIVATIVE_WINDOW = 5; // Samples
- const uint16_t MOVING_WINDOW_INTEGRATION = 13; // Samples
- const float THRESHOLD_FACTOR = 0.4; // Threshold multiplier
- /***** PPG PROCESSING CONSTANTS *****/
- const uint16_t PPG_LED_CURRENT = 18; // mA (0-255 for MAX30102)
- const uint8_t PPG_SPO2_COEFFICIENT = 17; // For SpO2 calculation
- const uint8_t PPG_SPO2_OFFSET = 104; // For SpO2 calculation
- /***** BP CALCULATION CONSTANTS (PTT-based) *****/
- const float PTT_TO_SBP_A = -0.5; // Calibration coefficient A
- const float PTT_TO_SBP_B = 150.0; // Calibration coefficient B
- const float PTT_TO_DBP_A = -0.3; // Calibration coefficient A
- const float PTT_TO_DBP_B = 90.0; // Calibration coefficient B
- /***** VITAL SIGN LIMITS *****/
- const uint8_t HR_MIN = 40;
- const uint8_t HR_MAX = 200;
- const uint8_t SPO2_MIN = 80;
- const uint8_t SPO2_MAX = 100;
- const uint8_t BP_SBP_NORMAL_MAX = 120;
- const uint8_t BP_DBP_NORMAL_MAX = 80;
- const uint8_t BP_SBP_ELEVATED_MAX = 129;
- const uint8_t BP_SBP_HIGH_MIN = 130;
- const uint8_t BP_DBP_HIGH_MIN = 80;
- /***** STRUCTS FOR DATA *****/
- typedef struct {
- uint16_t ecgBuffer[ECG_BUFFER_SIZE];
- uint16_t ppgBuffer[PPG_BUFFER_SIZE];
- uint16_t ecgIndex;
- uint16_t ppgIndex;
- uint32_t ecgSampleCount;
- uint32_t ppgSampleCount;
- uint32_t lastRPeakTime;
- uint32_t lastPPGPeakTime;
- float currentHR;
- float currentSpO2;
- uint8_t currentSBP;
- uint8_t currentDBP;
- bool ecgValid;
- bool ppgValid;
- float calibrationSBP;
- float calibrationDBP;
- } HealthData_t;
- typedef struct {
- float filteredECG[PAN_TOMPKINS_WINDOW];
- float derivativeECG[PAN_TOMPKINS_WINDOW];
- float squaredECG[PAN_TOMPKINS_WINDOW];
- float integratedECG[PAN_TOMPKINS_WINDOW];
- uint16_t filterIndex;
- float lowPassA;
- float lowPassB;
- float highPassA;
- float highPassB;
- } PanTompkins_t;
- typedef struct {
- uint16_t redBuffer[PPG_BUFFER_SIZE];
- uint16_t irBuffer[PPG_BUFFER_SIZE];
- uint8_t ppgIndex;
- float rValue;
- uint8_t lastPPGPeakDetected;
- } PPGData_t;
- /***** OLED DISPLAY OBJECT *****/
- U8G2_SH1106_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE, I2C_SCL, I2C_SDA);
- /***** GLOBAL VARIABLES *****/
- HealthData_t healthData;
- PanTompkins_t panTompkins;
- PPGData_t ppgData;
- EasyButton button(AD8232_ECG_PushButton_PIN_D4);
- QueueHandle_t dataQueue;
- TaskHandle_t ecgTaskHandle = NULL;
- TaskHandle_t serialTaskHandle = NULL;
- volatile bool systemRunning = true;
- // POTA OTA update management
- POTA pota;
- volatile bool otaAvailable = false;
- /***** TIMER VARIABLES *****/
- hw_timer_t *ecgTimer = NULL;
- hw_timer_t *ppgTimer = NULL;
- volatile uint32_t ecgTimerCount = 0;
- volatile uint32_t ppgTimerCount = 0;
- /****** INITIALIZATION FUNCTION *****/
- void initializeSystemVariables(void) {
- // Initialize health data structure
- healthData.ecgIndex = 0;
- healthData.ppgIndex = 0;
- healthData.ecgSampleCount = 0;
- healthData.ppgSampleCount = 0;
- healthData.lastRPeakTime = 0;
- healthData.lastPPGPeakTime = 0;
- healthData.currentHR = 0.0;
- healthData.currentSpO2 = 95.0;
- healthData.currentSBP = 120;
- healthData.currentDBP = 80;
- healthData.ecgValid = false;
- healthData.ppgValid = false;
- healthData.calibrationSBP = 120.0;
- healthData.calibrationDBP = 80.0;
- memset(healthData.ecgBuffer, 0, sizeof(healthData.ecgBuffer));
- memset(healthData.ppgBuffer, 0, sizeof(healthData.ppgBuffer));
- // Initialize Pan-Tompkins filter coefficients
- float wc = 2.0 * PI * LOW_PASS_FREQ / ECG_SAMPLE_RATE;
- panTompkins.lowPassA = wc / (2.0 + wc);
- panTompkins.lowPassB = wc / (2.0 + wc);
- wc = 2.0 * PI * HIGH_PASS_FREQ / ECG_SAMPLE_RATE;
- panTompkins.highPassA = 2.0 / (2.0 + wc);
- panTompkins.highPassB = (2.0 - wc) / (2.0 + wc);
- panTompkins.filterIndex = 0;
- memset(panTompkins.filteredECG, 0, sizeof(panTompkins.filteredECG));
- memset(panTompkins.derivativeECG, 0, sizeof(panTompkins.derivativeECG));
- memset(panTompkins.squaredECG, 0, sizeof(panTompkins.squaredECG));
- memset(panTompkins.integratedECG, 0, sizeof(panTompkins.integratedECG));
- // Initialize PPG data structure
- ppgData.ppgIndex = 0;
- ppgData.rValue = 1.0;
- ppgData.lastPPGPeakDetected = 0;
- memset(ppgData.redBuffer, 0, sizeof(ppgData.redBuffer));
- memset(ppgData.irBuffer, 0, sizeof(ppgData.irBuffer));
- }
- /****** ECG TIMER INTERRUPT *****/
- void IRAM_ATTR ecgTimerISR(void) {
- ecgTimerCount++;
- if (ecgTimerCount % (80000000 / ECG_SAMPLE_RATE) == 0) {
- readECGSignal();
- }
- }
- /****** PPG TIMER INTERRUPT *****/
- void IRAM_ATTR ppgTimerISR(void) {
- ppgTimerCount++;
- if (ppgTimerCount % (80000000 / PPG_SAMPLE_RATE) == 0) {
- readPPGSignal();
- }
- }
- /****** POTA OTA AVAILABLE CALLBACK - Called when OTA update is available *****/
- void onOTAAvailableCallback(const char* version) {
- // Called when POTA detects a new firmware update is available
- // The version parameter contains the available firmware version
- Serial.print("[POTA] OTA update available! Version: ");
- Serial.println(version);
- Serial.println("[POTA] Flag set for restart.");\n otaAvailable = true;
- }
- /****** SETUP FUNCTION *****/
- void setup(void) {
- // Initialize serial communication at 115200 baud
- Serial.begin(115200);
- delay(500);
- Serial.println("\n\n========== ECG+PPG Health Monitor Initializing ==========");
- // Initialize pins
- pinMode(AD8232_ECG_PushButton_PIN_D4, INPUT_PULLUP);
- pinMode(MAX30102_PPG_LED_PIN_D13, OUTPUT);
- pinMode(SH1106_OLED_LEDRGB_Red_PIN_D14, OUTPUT);
- pinMode(SH1106_OLED_LEDRGB_Green_PIN_RX2, OUTPUT);
- pinMode(SH1106_OLED_LEDRGB_Blue_PIN_TX2, OUTPUT);
- // Initialize I2C
- Wire.begin(I2C_SDA, I2C_SCL);
- Wire.setClock(400000);
- Serial.println("[INIT] I2C initialized at 400kHz");
- // Initialize system variables
- initializeSystemVariables();
- Serial.println("[INIT] System variables initialized");
- // Initialize OLED display
- initializeOLED();
- Serial.println("[INIT] OLED display initialized");
- // Initialize MAX30102 sensor
- initializeMAX30102();
- Serial.println("[INIT] MAX30102 PPG sensor initialized");
- // Initialize button
- button.begin();
- Serial.println("[INIT] EasyButton initialized");
- // Initialize POTA OTA update system with WiFi credentials from secrets.h
- Serial.println("[POTA] Initializing OTA update system...");
- pota.begin(DEVICE_TYPE, FIRMWARE_VERSION, AUTH_TOKEN, SERVER_SECRET, WIFI_SSID, WIFI_PASSWORD);
- Serial.println("[POTA] POTA initialized with WiFi credentials from secrets.h");
- // Register OTA available callback - callback accepts const char* version parameter
- pota.onOTAAvailable(onOTAAvailableCallback);
- Serial.println("[POTA] OTA callback registered");
- // Check for OTA updates immediately after initialization (MANDATORY)
- Serial.println("[POTA] Checking for available OTA updates...");
- pota.checkAndPerformOTA();
- Serial.println("[POTA] OTA check completed");
- // Create data queue for inter-task communication
- dataQueue = xQueueCreate(10, sizeof(HealthData_t));
- Serial.println("[INIT] Message queue created");
- // Create FreeRTOS tasks
- xTaskCreatePinnedToCore(
- ecgTask,
- "ECG_TASK",
- 4096,
- NULL,
- 3,
- &ecgTaskHandle,
- 0 // Core 0: Sensor acquisition and UI updates
- );
- xTaskCreatePinnedToCore(
- serialTask,
- "SERIAL_TASK",
- 2048,
- NULL,
- 2,
- &serialTaskHandle,
- 1 // Core 1: Debug serial output
- );
- Serial.println("[INIT] FreeRTOS tasks created");
- Serial.println("========== System Ready ==========\n");
- Serial.println("Serial Calibration Command: CAL SBP:120 DBP:80");
- Serial.println("(Replace values with measured SBP and DBP)\n");
- // Set initial RGB color (green)
- setRGBColor(0, 255, 0);
- }
- /****** MAIN LOOP (RUNS ON CORE 1) *****/
- void loop(void) {
- // Call POTA loop to handle background OTA operations
- pota.loop();
- // Check if OTA update is available and needs restart
- if (otaAvailable) {
- Serial.println("[POTA] OTA update available - initiating device restart for update...");
- setRGBColor(255, 0, 255); // Magenta: OTA in progress
- delay(500);
- // Call POTA restart to reboot into OTA mode
- pota.restart();
- // This will not return - device reboots
- }
- vTaskDelay(1000 / portTICK_PERIOD_MS);
- }
- /****** ECG AND SENSOR ACQUISITION TASK (CORE 0) *****/
- void ecgTask(void *pvParameters) {
- TickType_t xLastWakeTime = xTaskGetTickCount();
- const TickType_t xFrequency = pdMS_TO_TICKS(10);
- Serial.println("[TASK] ECG Task started on Core 0");
- while (systemRunning) {
- // Handle button events
- button.read();
- // Process ECG data when buffer is half full
- if (healthData.ecgIndex >= ECG_BUFFER_SIZE / 2) {
- processPanTompkins();
- }
- // Process PPG data when buffer is half full
- if (healthData.ppgIndex >= PPG_BUFFER_SIZE / 2) {
- processPPG();
- }
- // Calculate vitals
- if (healthData.ecgValid && healthData.ppgValid) {
- calculateVitals();
- }
- // Update OLED display
- displayDashboard();
- xTaskDelayUntil(&xLastWakeTime, xFrequency);
- }
- vTaskDelete(NULL);
- }
- /****** SERIAL DEBUG AND CALIBRATION TASK (CORE 1) *****/
- void serialTask(void *pvParameters) {
- Serial.println("[TASK] Serial Task started on Core 1");
- while (systemRunning) {
- // Handle serial calibration commands
- if (Serial.available()) {
- handleSerialCalibration();
- }
- // Output debug info periodically
- static uint32_t lastDebugTime = 0;
- if (millis() - lastDebugTime > 5000) {
- logDebugInfo();
- lastDebugTime = millis();
- }
- vTaskDelay(100 / portTICK_PERIOD_MS);
- }
- vTaskDelete(NULL);
- }
- /****** OLED INITIALIZATION *****/
- void initializeOLED(void) {
- u8g2.begin();
- u8g2.setFont(u8g2_font_ncenB10_tr);
- u8g2.clearBuffer();
- u8g2.drawStr(10, 32, "Initializing...");
- u8g2.sendBuffer();
- Serial.println("[OLED] Display initialized at I2C 0x3C");
- }
- /****** MAX30102 INITIALIZATION *****/
- void initializeMAX30102(void) {
- // Reset MAX30102
- Wire.beginTransmission(MAX30102_ADDRESS);
- Wire.write(0x09); // Mode Configuration Register
- Wire.write(0x40); // Reset bit
- Wire.endTransmission();
- delay(100);
- // Configure for dual mode (HR + SpO2)
- Wire.beginTransmission(MAX30102_ADDRESS);
- Wire.write(0x09); // Mode Configuration Register
- Wire.write(0x03); // SpO2 mode
- Wire.endTransmission();
- // Set LED currents
- Wire.beginTransmission(MAX30102_ADDRESS);
- Wire.write(0x0C); // LED1 Pulse Amplitude (RED)
- Wire.write(PPG_LED_CURRENT);
- Wire.endTransmission();
- Wire.beginTransmission(MAX30102_ADDRESS);
- Wire.write(0x0D); // LED2 Pulse Amplitude (IR)
- Wire.write(PPG_LED_CURRENT);
- Wire.endTransmission();
- // Set sample rate to 100Hz
- Wire.beginTransmission(MAX30102_ADDRESS);
- Wire.write(0x08); // FIFO Configuration
- Wire.write(0x0F); // Sample averaging and FIFO rollover
- Wire.endTransmission();
- // Clear FIFO pointers
- Wire.beginTransmission(MAX30102_ADDRESS);
- Wire.write(0x04); // FIFO Write Pointer
- Wire.write(0x00);
- Wire.endTransmission();
- Wire.beginTransmission(MAX30102_ADDRESS);
- Wire.write(0x06); // FIFO Read Pointer
- Wire.write(0x00);
- Wire.endTransmission();
- delay(100);
- Serial.println("[MAX30102] PPG sensor configured");
- }
- /****** READ ECG SIGNAL FROM GPIO34 *****/
- void readECGSignal(void) {
- // Read analog ECG signal from GPIO34
- uint16_t ecgRaw = analogRead(ECG_SIGNAL_PIN);
- // Validate ECG signal (should be in reasonable range, not saturated)
- if (ecgRaw > 200 && ecgRaw < 3900) {
- healthData.ecgValid = true;
- } else if (ecgRaw <= 200 || ecgRaw >= 3900) {
- healthData.ecgValid = false;
- }
- // Store in circular buffer
- healthData.ecgBuffer[healthData.ecgIndex] = ecgRaw;
- healthData.ecgIndex = (healthData.ecgIndex + 1) % ECG_BUFFER_SIZE;
- healthData.ecgSampleCount++;
- }
- /****** READ PPG SIGNAL FROM MAX30102 *****/
- void readPPGSignal(void) {
- uint8_t readPtr, writePtr, numSamples;
- // Read FIFO pointers
- Wire.beginTransmission(MAX30102_ADDRESS);
- Wire.write(0x04); // Write Pointer
- Wire.endTransmission();
- Wire.requestFrom(MAX30102_ADDRESS, (uint8_t)1);
- writePtr = Wire.read();
- Wire.beginTransmission(MAX30102_ADDRESS);
- Wire.write(0x06); // Read Pointer
- Wire.endTransmission();
- Wire.requestFrom(MAX30102_ADDRESS, (uint8_t)1);
- readPtr = Wire.read();
- numSamples = (writePtr - readPtr) & 0x1F;
- // Read FIFO data
- for (uint8_t i = 0; i < numSamples; i++) {
- Wire.beginTransmission(MAX30102_ADDRESS);
- Wire.write(0x07); // FIFO Data Register
- Wire.endTransmission();
- Wire.requestFrom(MAX30102_ADDRESS, (uint8_t)6);
- uint32_t redData = (Wire.read() << 16) | (Wire.read() << 8) | Wire.read();
- uint32_t irData = (Wire.read() << 16) | (Wire.read() << 8) | Wire.read();
- redData &= 0x03FFFF; // 18-bit mask
- irData &= 0x03FFFF; // 18-bit mask
- // Validate PPG signal
- if (irData > 5000 && irData < 200000) {
- healthData.ppgValid = true;
- } else {
- healthData.ppgValid = false;
- }
- // Store in circular buffer
- ppgData.redBuffer[healthData.ppgIndex] = redData >> 8;
- ppgData.irBuffer[healthData.ppgIndex] = irData >> 8;
- healthData.ppgIndex = (healthData.ppgIndex + 1) % PPG_BUFFER_SIZE;
- healthData.ppgSampleCount++;
- }
- }
- /****** PAN-TOMPKINS ECG PROCESSING *****/
- void processPanTompkins(void) {
- // Apply low-pass filter (cutoff ~5 Hz)
- float filteredValue;
- static float prevFiltered = 0;
- for (uint16_t i = 0; i < ECG_BUFFER_SIZE / 2; i++) {
- uint16_t idx = (healthData.ecgIndex + i) % ECG_BUFFER_SIZE;
- float ecgValue = (float)healthData.ecgBuffer[idx] / 4095.0 * 3.3;
- // Butterworth low-pass filter
- filteredValue = panTompkins.lowPassA * ecgValue +
- (1.0 - panTompkins.lowPassA) * prevFiltered;
- prevFiltered = filteredValue;
- panTompkins.filteredECG[panTompkins.filterIndex] = filteredValue;
- // Apply high-pass filter
- static float prevHighPass = 0;
- float highPassed = panTompkins.highPassA * (filteredValue - prevHighPass) +
- panTompkins.highPassB * prevHighPass;
- prevHighPass = filteredValue;
- // Derivative (5-point difference)
- if (panTompkins.filterIndex >= DERIVATIVE_WINDOW) {
- panTompkins.derivativeECG[panTompkins.filterIndex] =
- (highPassed - panTompkins.filteredECG[panTompkins.filterIndex - DERIVATIVE_WINDOW]) /
- (DERIVATIVE_WINDOW * (1.0 / ECG_SAMPLE_RATE));
- }
- // Squaring
- panTompkins.squaredECG[panTompkins.filterIndex] =
- panTompkins.derivativeECG[panTompkins.filterIndex] *
- panTompkins.derivativeECG[panTompkins.filterIndex];
- // Moving window integration
- if (panTompkins.filterIndex >= MOVING_WINDOW_INTEGRATION) {
- float sum = 0;
- for (uint8_t j = 0; j < MOVING_WINDOW_INTEGRATION; j++) {
- uint16_t idx = (panTompkins.filterIndex - j) % PAN_TOMPKINS_WINDOW;
- sum += panTompkins.squaredECG[idx];
- }
- panTompkins.integratedECG[panTompkins.filterIndex] =
- sum / MOVING_WINDOW_INTEGRATION;
- // R-peak detection with threshold
- static float threshold = 0.01;
- if (panTompkins.integratedECG[panTompkins.filterIndex] > threshold) {
- // Check if this is a local maximum (avoid duplicate detections)
- if (panTompkins.filterIndex > 0 &&
- panTompkins.integratedECG[panTompkins.filterIndex] >
- panTompkins.integratedECG[(panTompkins.filterIndex - 1) % PAN_TOMPKINS_WINDOW]) {
- uint32_t currentTime = millis();
- // RR interval validation (HR between 40-200 bpm = 300-1500ms)
- if (healthData.lastRPeakTime > 0) {
- uint32_t rrInterval = currentTime - healthData.lastRPeakTime;
- if (rrInterval >= 300 && rrInterval <= 1500) {
- healthData.lastRPeakTime = currentTime;
- }
- } else {
- healthData.lastRPeakTime = currentTime;
- }
- // Update adaptive threshold
- threshold = THRESHOLD_FACTOR * panTompkins.integratedECG[panTompkins.filterIndex];
- }
- }
- }
- panTompkins.filterIndex = (panTompkins.filterIndex + 1) % PAN_TOMPKINS_WINDOW;
- }
- }
- /****** PPG SIGNAL PROCESSING *****/
- void processPPG(void) {
- // Calculate SpO2 ratio (R = (AC_red/DC_red) / (AC_ir/DC_ir))
- float dcRed = 0, dcIR = 0, acRed = 0, acIR = 0;
- float meanRed = 0, meanIR = 0;
- // Calculate mean (DC components)
- for (uint16_t i = 0; i < PPG_BUFFER_SIZE; i++) {
- meanRed += ppgData.redBuffer[i];
- meanIR += ppgData.irBuffer[i];
- }
- meanRed /= PPG_BUFFER_SIZE;
- meanIR /= PPG_BUFFER_SIZE;
- dcRed = meanRed;
- dcIR = meanIR;
- // Calculate AC components (peak-to-peak)
- float maxRed = 0, minRed = 65535;
- float maxIR = 0, minIR = 65535;
- for (uint16_t i = 0; i < PPG_BUFFER_SIZE; i++) {
- if (ppgData.redBuffer[i] > maxRed) maxRed = ppgData.redBuffer[i];
- if (ppgData.redBuffer[i] < minRed) minRed = ppgData.redBuffer[i];
- if (ppgData.irBuffer[i] > maxIR) maxIR = ppgData.irBuffer[i];
- if (ppgData.irBuffer[i] < minIR) minIR = ppgData.irBuffer[i];
- }
- acRed = maxRed - minRed;
- acIR = maxIR - minIR;
- // Prevent division by zero
- if (dcRed > 0 && dcIR > 0 && acRed > 0 && acIR > 0) {
- ppgData.rValue = (acRed / dcRed) / (acIR / dcIR);
- // Calculate SpO2 using empirical formula
- // SpO2 = 104 - 17*R
- float calculatedSpO2 = PPG_SPO2_OFFSET - PPG_SPO2_COEFFICIENT * ppgData.rValue;
- // Validate SpO2 range
- if (calculatedSpO2 >= SPO2_MIN && calculatedSpO2 <= SPO2_MAX) {
- healthData.currentSpO2 = calculatedSpO2;
- } else if (calculatedSpO2 < SPO2_MIN) {
- healthData.currentSpO2 = SPO2_MIN;
- } else {
- healthData.currentSpO2 = SPO2_MAX;
- }
- // Detect PPG peak
- if (maxIR > meanIR + (acIR / 2)) {
- healthData.lastPPGPeakTime = millis();
- ppgData.lastPPGPeakDetected = 1;
- }
- }
- }
- /****** CALCULATE VITAL SIGNS *****/
- void calculateVitals(void) {
- // Calculate Heart Rate from R-R interval
- if (healthData.lastRPeakTime > 0) {
- uint32_t currentTime = millis();
- uint32_t rrInterval = currentTime - healthData.lastRPeakTime;
- if (rrInterval > 0) {
- healthData.currentHR = 60000.0 / rrInterval;
- // Validate HR range
- if (healthData.currentHR < HR_MIN || healthData.currentHR > HR_MAX) {
- healthData.currentHR = 0;
- }
- }
- }
- // Calculate Blood Pressure from PTT
- if (healthData.lastRPeakTime > 0 && healthData.lastPPGPeakTime > 0) {
- int32_t ptt = (int32_t)healthData.lastPPGPeakTime - (int32_t)healthData.lastRPeakTime;
- // PTT should be positive (PPG peak after R-peak)
- if (ptt > 0 && ptt < 1000) {
- // Convert PTT (ms) to PTT-based BP with calibration
- healthData.currentSBP = (uint8_t)constrain(
- (PTT_TO_SBP_A * ptt) + PTT_TO_SBP_B + healthData.calibrationSBP,
- 60, 200
- );
- healthData.currentDBP = (uint8_t)constrain(
- (PTT_TO_DBP_A * ptt) + PTT_TO_DBP_B + healthData.calibrationDBP,
- 40, 120
- );
- }
- }
- }
- /****** DISPLAY CLEAN DASHBOARD ON OLED (NO CALIBRATION UI) *****/
- void displayDashboard(void) {
- static uint32_t lastDisplayTime = 0;
- uint32_t currentTime = millis();
- // Update display every 500ms to reduce flicker
- if (currentTime - lastDisplayTime < 500) {
- return;
- }
- lastDisplayTime = currentTime;
- u8g2.clearBuffer();
- // Title line (small font)
- u8g2.setFont(u8g2_font_ncenB08_tr);
- u8g2.drawStr(0, 10, "Vital Signs");
- // Separator line to prevent overlap
- u8g2.drawHLine(0, 12, 128);
- // HR line (Heart Rate in bpm) - large font
- u8g2.setFont(u8g2_font_ncenB14_tr);
- char hrStr[10];
- snprintf(hrStr, sizeof(hrStr), "%3.0f", healthData.currentHR);
- u8g2.drawStr(5, 32, hrStr);
- u8g2.setFont(u8g2_font_ncenB10_tr);
- u8g2.drawStr(50, 32, "bpm");
- // SpO2 line (Oxygen Saturation) - large font
- u8g2.setFont(u8g2_font_ncenB14_tr);
- char spo2Str[8];
- snprintf(spo2Str, sizeof(spo2Str), "%3.0f%%", healthData.currentSpO2);
- u8g2.drawStr(5, 48, spo2Str);
- // BP line (Blood Pressure) - medium font
- u8g2.setFont(u8g2_font_ncenB12_tr);
- char bpStr[12];
- snprintf(bpStr, sizeof(bpStr), "%3d/%2d", healthData.currentSBP, healthData.currentDBP);
- u8g2.drawStr(5, 62, bpStr);
- // Status indicators on the right side (small font, no overlap)
- u8g2.setFont(u8g2_font_ncenB08_tr);
- u8g2.drawStr(100, 32, healthData.ecgValid ? "ECG" : "---");
- u8g2.drawStr(100, 48, healthData.ppgValid ? "PPG" : "---");
- // Send to display
- u8g2.sendBuffer();
- }
- /****** HANDLE SERIAL CALIBRATION (CAL COMMAND ONLY) *****/
- void handleSerialCalibration(void) {
- static String calibrationBuffer = "";
- while (Serial.available()) {
- char inChar = Serial.read();
- Serial.write(inChar); // Echo the input
- if (inChar == '\n' || inChar == '\r') {
- if (calibrationBuffer.length() > 0) {
- calibrationBuffer.toUpperCase();
- // Parse calibration command: CAL SBP:XXX DBP:YYY
- if (calibrationBuffer.startsWith("CAL")) {
- // Find SBP and DBP indices
- int sbpIndex = calibrationBuffer.indexOf("SBP:");
- int dbpIndex = calibrationBuffer.indexOf("DBP:");
- if (sbpIndex >= 0 && dbpIndex >= 0) {
- // Extract SBP value
- String sbpStr = calibrationBuffer.substring(sbpIndex + 4);
- sbpStr = sbpStr.substring(0, sbpStr.indexOf(" "));
- // Extract DBP value
- String dbpStr = calibrationBuffer.substring(dbpIndex + 4);
- int calibSBP = sbpStr.toInt();
- int calibDBP = dbpStr.toInt();
- // Validate ranges (realistic BP values)
- if (calibSBP > 60 && calibSBP < 200 && calibDBP > 40 && calibDBP < 120) {
- healthData.calibrationSBP = calibSBP;
- healthData.calibrationDBP = calibDBP;
- Serial.println("");
- Serial.print("[CALIB] SUCCESS: SBP=");
- Serial.print(calibSBP);
- Serial.print(", DBP=");
- Serial.println(calibDBP);
- } else {
- Serial.println("");
- Serial.println("[CALIB] ERROR: Invalid range. Use 60<SBP<200, 40<DBP<120");
- }
- } else {
- Serial.println("");
- Serial.println("[CALIB] ERROR: Format: CAL SBP:120 DBP:80");
- }
- }
- calibrationBuffer = "";
- }
- } else {
- calibrationBuffer += inChar;
- }
- }
- }
- /****** SET RGB COLOR (STATUS INDICATOR) *****/
- void setRGBColor(uint8_t red, uint8_t green, uint8_t blue) {
- analogWrite(SH1106_OLED_LEDRGB_Red_PIN_D14, red);
- analogWrite(SH1106_OLED_LEDRGB_Green_PIN_RX2, green);
- analogWrite(SH1106_OLED_LEDRGB_Blue_PIN_TX2, blue);
- }
- /****** LOG DEBUG INFORMATION (SERIAL OUTPUT) *****/
- void logDebugInfo(void) {
- Serial.println("\n========== SYSTEM STATUS ==========");
- Serial.print("[TIME] Uptime: ");
- Serial.print(millis() / 1000);
- Serial.println(" seconds");
- Serial.print("[ECG] Samples: ");
- Serial.print(healthData.ecgSampleCount);
- Serial.print(" | Valid: ");
- Serial.println(healthData.ecgValid ? "YES" : "NO");
- Serial.print("[PPG] Samples: ");
- Serial.print(healthData.ppgSampleCount);
- Serial.print(" | Valid: ");
- Serial.println(healthData.ppgValid ? "YES" : "NO");
- Serial.print("[VITALS] HR: ");
- Serial.print(healthData.currentHR, 1);
- Serial.print(" bpm | SpO2: ");
- Serial.print(healthData.currentSpO2, 1);
- Serial.println(" %");
- Serial.print("[BP] SBP: ");
- Serial.print(healthData.currentSBP);
- Serial.print(" mmHg | DBP: ");
- Serial.print(healthData.currentDBP);
- Serial.println(" mmHg");
- Serial.print("[CALIB] SBP: ");
- Serial.print(healthData.calibrationSBP, 1);
- Serial.print(" | DBP: ");
- Serial.print(healthData.calibrationDBP, 1);
- Serial.println("");
- Serial.println("====================================\n");
- // Update RGB LED based on BP status for visual feedback
- if (healthData.currentSBP <= BP_SBP_NORMAL_MAX &&
- healthData.currentDBP <= BP_DBP_NORMAL_MAX) {
- setRGBColor(0, 255, 0); // Green - Normal BP
- } else if (healthData.currentSBP <= BP_SBP_ELEVATED_MAX) {
- setRGBColor(255, 255, 0); // Yellow - Elevated BP
- } else {
- setRGBColor(255, 0, 0); // Red - High BP
- }
- }
- /* END CODE */
Advertisement
Add Comment
Please, Sign In to add comment