eC Mastermind
Published © MIT

Blood Oxygen Saturation

The Guardian Grip is a low-power, RP2040-based wearable SpO2 monitor designed for nocturnal hypoxia detection and smart home safety interven

BeginnerWork in progress5 days61
Blood Oxygen Saturation

Things used in this project

Story

Read more

Schematics

Fritzing Circuit

It uses the MAX30102 to measure the user's pulse and blood oxygen saturation (SpO2).

It uses the OLED display to show the vital signs in real-time.

It uses the Buzzer as an active alert mechanism if critical thresholds (like low SpO2, or Nocturnal Hypoxia) are detected.

Code

CodeRPIMbed OS.ino

C/C++
Arduino IDE 2.3.6
#include <Arduino.h>
#include <Wire.h>
#include "max30102_driver.h"
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

// Pin constants for TwoWire on Pico (your values already used)
const int SDA_PIN = 8;
const int SCL_PIN = 9;
TwoWire CustomWire(SDA_PIN, SCL_PIN);
// OLED display setup
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 32
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &CustomWire, -1);

// MAX30102 sensor driver
MAX30102_Driver sensor;
bool ok = false;

// buffers & app state
#define BUF_SIZE 100
uint32_t redBuf[BUF_SIZE];
uint32_t irBuf[BUF_SIZE];
int bufIndex = 0;
uint32_t totalSamples = 0;
bool dataStarted = false;
int spo2 = 0, hr = 0;
unsigned long lastDisplay = 0;
unsigned long lastDiag = 0;

// small waveform drawing buffer to fit 128 width
#define WAVE_SAMPLES 80            // narrow waveform to fit comfortably
uint16_t wf[WAVE_SAMPLES];
uint8_t wfIndex = 0;
uint8_t wfFilled = 0;

// Alarm and Smart Home integration
const int SPO2_WARNING_THRESHOLD = 90;  // change to your preference
const int ALARM_PIN = 15;               // example output pin to trigger a buzzer or a relay
bool alarmActive = false;

// Function declarations
void calculateSpO2Simple();
void updateDisplayStyled();
void checkSpO2AndAlert();
void publishSmartHomeEvent(const char* state);

void setup() {
  Serial.begin(115200);
  delay(1500);

  CustomWire.begin();
  CustomWire.setClock(400000);
  delay(200);

  // initialize display
  if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)){
    Serial.println("No OLED found");
    while(1) delay(100);
  }
  display.clearDisplay();
  display.setTextColor(SSD1306_WHITE);
  display.setTextSize(1);

  // initialize driver & hardware
  pinMode(ALARM_PIN, OUTPUT);
  digitalWrite(ALARM_PIN, LOW);

  ok = sensor.begin(CustomWire);
  if (!ok) {
    Serial.println("Sensor init failed (part id mismatch?)");
    display.setCursor(0,0);
    display.println("Sensor init failed");
    display.display();
    while (1) delay(1000);
  }
  Serial.println("Sensor ready. Place finger on sensor.");

  display.clearDisplay();
  display.setCursor(0, 0);
  display.setTextSize(1);
  display.println("MAX30102 Ready");
  display.println("Place finger...");
  display.display();
}

void loop() {
  // periodic diagnostics print
  if (millis() - lastDiag > 2000) {
    lastDiag = millis();
    sensor.printDiagnostics();
  }

  // read FIFO up to 4 samples
  ppg_data_t samples[4];
  uint8_t n = sensor.readFIFO(samples, 4);

  if (n > 0 && !dataStarted) {
    dataStarted = true;
    Serial.println("First data arrived!");
  }

  // process samples
  for (uint8_t i = 0; i < n; ++i) {
    redBuf[bufIndex] = samples[i].red;
    irBuf[bufIndex] = samples[i].ir;
    ++bufIndex;
    ++totalSamples;

    // store normalized waveform sample (IR) into wf buffer
    // Do a simple scaling to the 0..(SCREEN_HEIGHT-8) range; store as 0..24
    // (We maintain wfIndex on updates and wrap/cycle as new samples appear)
    {
      // small smoothing & normalization from buffer
      uint32_t low = irBuf[0], high = irBuf[0];
      for (int k = 1; k < bufIndex; ++k) {
        if (irBuf[k] > high) high = irBuf[k];
        if (irBuf[k] < low) low = irBuf[k];
      }
      uint32_t range = (high - low);
      uint16_t scaled = 0;
      if (range > 0) {
        // map current IR into 0..(SCREEN_HEIGHT-10); leave some space
        scaled = (uint16_t)((samples[i].ir - low) * (SCREEN_HEIGHT - 10) / range);
      }
      wf[wfIndex] = scaled;
      wfIndex = (wfIndex + 1) % WAVE_SAMPLES;
      if (!wfFilled && wfIndex == 0) wfFilled = 1;
    }

    Serial.print("Red: "); Serial.print(samples[i].red);
    Serial.print(" IR: "); Serial.println(samples[i].ir);

    if (bufIndex >= BUF_SIZE) {
      bufIndex = 0;
      calculateSpO2Simple();
    }
  }

  // update OLED every 500 ms
  if (millis() - lastDisplay > 500) {
    lastDisplay = millis();
    updateDisplayStyled();
    checkSpO2AndAlert();
  }
  delay(10);
}

/*
  Simple SpO2 estimate - keep this for now, you can replace with a better algorithm later
*/
void calculateSpO2Simple() {
  uint64_t rSum=0, iSum=0;
  for (int i=0;i<BUF_SIZE;i++){ rSum+=redBuf[i]; iSum+=irBuf[i]; }
  uint32_t rAvg = rSum / BUF_SIZE;
  uint32_t iAvg = iSum / BUF_SIZE;
  if (iAvg==0) return;

  uint32_t rMax = redBuf[0], rMin = redBuf[0];
  uint32_t iMax = irBuf[0], iMin = irBuf[0];
  for (int i=1;i<BUF_SIZE;i++){
    if (redBuf[i] > rMax) rMax = redBuf[i];
    if (redBuf[i] < rMin) rMin = redBuf[i];
    if (irBuf[i] > iMax) iMax = irBuf[i];
    if (irBuf[i] < iMin) iMin = irBuf[i];
  }
  uint32_t rAC = rMax - rMin;
  uint32_t iAC = iMax - iMin;
  if (iAC==0) return;
  float ratio = ((float)rAC / rAvg) / ((float)iAC / iAvg);
  int spo2_est = (int)(110.0f - 25.0f * ratio);
  if (spo2_est < 80) spo2_est = 80;
  if (spo2_est > 100) spo2_est = 100;
  spo2 = spo2_est;
  hr = 60 + (rAC / 1000); if (hr<40) hr=40; if (hr>200) hr=200;
  Serial.print(">> Est SpO2: "); Serial.print(spo2);
  Serial.print(" HR: "); Serial.println(hr);
}

/*
  Styled display:
  - Large centered SPO2
  - small HR on the bottom left
  - waveform across bottom of screen (80 samples)
  - threshold inversion (display invert) and alarm pin when SPO2 below threshold
*/
void updateDisplayStyled() {
  display.clearDisplay();

  // If no data yet, show message
  if (!dataStarted) {
    display.setTextSize(1);
    display.setCursor(0, 0);
    display.println("MAX30102 Ready");
    display.println("Place finger...");
    display.display();
    return;
  }

  // Draw large Spo2 centered
  display.setTextSize(3);
  display.setTextColor(SSD1306_WHITE);

  char spo2Text[8];
  snprintf(spo2Text, sizeof(spo2Text), "%d%%", spo2);
  int16_t x1, y1;
  uint16_t w, h;
  display.getTextBounds(spo2Text, 0, 0, &x1, &y1, &w, &h);
  int16_t x = (SCREEN_WIDTH - w) / 2;
  int16_t y = 0;
  display.setCursor(x, y);
  display.print(spo2Text);

  // Draw HR on lower-right small font
  display.setTextSize(1);
  String hrText = String("HR: ") + String(hr);
  display.getTextBounds(hrText.c_str(), 0, 0, &x1, &y1, &w, &h);
  // place HR in bottom-right corner (with a small margin)
  int16_t hrX = SCREEN_WIDTH - w - 2;
  int16_t hrY = SCREEN_HEIGHT - h - 1;
  display.setCursor(hrX, hrY);
  display.print(hrText);

  // Draw mini waveform across bottom (height - 8px area)
  int waveTop = SCREEN_HEIGHT - 8;  // keep wave narrow
  int sw = SCREEN_WIDTH;
  int samplesToDraw = wfFilled ? WAVE_SAMPLES : wfIndex;
  if (samplesToDraw > 0) {
    // step across available width
    for (int i = 0; i < sw; i++) {
      // map screen column to waveform index (most recent to right)
      int idx = (wfIndex + i) % WAVE_SAMPLES;
      // scale sample to 0..8 height
      uint8_t val = (wf[idx] & 0xff);
      uint8_t hgt = map(val, 0, (SCREEN_HEIGHT - 10), 0, 7);
      // draw single column from bottom up
      for (int yOff = 0; yOff < hgt; yOff++) {
        display.drawPixel(i, waveTop - yOff, SSD1306_WHITE);
      }
    }
  }

  // draw a small threshold mark next to SPO2
  if (spo2 <= SPO2_WARNING_THRESHOLD) {
    // invert the whole screen briefly or place '!' icon
    // but to avoid flicker, we will draw a small triangle instead
    display.fillTriangle(2, 2, 10, 2, 6, 12, SSD1306_WHITE);
    display.setCursor(14, 2);
    display.setTextSize(1);
    display.print("LOW");
  }

  display.display();
}

/*
  Check threshold and trigger simple alarm and Smart Home event (Serial JSON).
  Alarm pin will be set HIGH if SPO2 < threshold.
*/
void checkSpO2AndAlert() {
  bool shouldAlarm = (spo2 <= SPO2_WARNING_THRESHOLD);
  if (shouldAlarm && !alarmActive) {
    alarmActive = true;
    digitalWrite(ALARM_PIN, HIGH);
    //Serial.print("{\"event\":\"SPO2_ALERT\",\"state\":\"ON\",\"spo2\":%d,\"hr\":%d}\n", spo2, hr);
  } else if (!shouldAlarm && alarmActive) {
    alarmActive = false;
    digitalWrite(ALARM_PIN, LOW);
    //Serial.print("{\"event\":\"SPO2_ALERT\",\"state\":\"OFF\",\"spo2\":%d,\"hr\":%d}\n", spo2, hr);
  }
}

// Optional: publish JSON style message to any attached Smart Home Bridge:
// This example uses Serial, but you can change it to MQTT or WiFi.
void publishSmartHomeEvent(const char* state) {
  Serial.print("{\"event\":\"SPO2\",\"state\":\"");
  Serial.print(state);
  Serial.print("\",\"spo2\":");
  Serial.print(spo2);
  Serial.print(",\"hr\":");
  Serial.print(hr);
  Serial.println("}");
}

Credits

eC Mastermind
1 project • 0 followers

Comments