Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Inconsistent Beat Detection Issue with PulseSensor for PTT Measurement #202

Open
szhu07 opened this issue Feb 21, 2025 · 13 comments
Open

Inconsistent Beat Detection Issue with PulseSensor for PTT Measurement #202

szhu07 opened this issue Feb 21, 2025 · 13 comments

Comments

@szhu07
Copy link

szhu07 commented Feb 21, 2025

I'm writing to report an issue I'm experiencing while using two PulseSensors to measure Pulse Transit Time (PTT). I have the sensors set up with one on the wrist and one on the fingertip, using an Arduino Uno for data collection.
The main issue I'm encountering is inconsistent beat detection between the two sensors. What typically happens is that one sensor gets "stuck" in a beat-detected state while the other sensor fails to detect beats. For example, I'll see the wrist sensor continuously showing as detected while the fingertip sensor shows no detection, or vice versa. Initially, this was partially due to having the sensors connected to different voltage levels (3.3V and 5V), but the issue persists even after correcting this by properly splitting the 5V supply to both sensors using a breadboard.
The signal levels from both sensors appear strong and within normal ranges (wrist: 502-526, fingertip: 431-609), but the beat detection remains inconsistent. I've tried adjusting the detection thresholds (currently at 475 for wrist and 400 for fingertip) and implementing timeout mechanisms to reset stuck detections, but the problem continues.
This inconsistent detection prevents accurate PTT calculation since it requires properly timed beats from both sensors. Could you advise on potential solutions or if there might be other factors I should consider regarding sensor placement, contact pressure, or ambient light effects?
Thank you for your assistance.

@biomurph
Copy link
Contributor

@szhu07
Thanks for using PulseSensor in your PTT application.
I need more information from you in order to help out.

  • Please post a photo or two of your hardware and setup?
  • Please post a screenshot of the pulse waveforms you are seeing? Arduino plotter or our Processing sketch for two PulseSensors.

The wrist is not a super ideal place to try to get PPG from. It can be done, but the signals on the wrist are not as strong as signals from parts of the body with capillary tissues. Have you tried using fingertip/earlobe for PTT?

@szhu07
Copy link
Author

szhu07 commented Feb 21, 2025

@szhu07 Thanks for using PulseSensor in your PTT application. I need more information from you in order to help out.

  • Please post a photo or two of your hardware and setup?
  • Please post a screenshot of the pulse waveforms you are seeing? Arduino plotter or our Processing sketch for two PulseSensors.

The wrist is not a super ideal place to try to get PPG from. It can be done, but the signals on the wrist are not as strong as signals from parts of the body with capillary tissues. Have you tried using fingertip/earlobe for PTT?

@biomurph
Hi! When I try to plot the waveforms with Arduino plotter and the Processing sketch, nothing is showing up. I will send pictures of my hardware connection and you can find the code I'm using in my public repo!

https://github.com/szhu07/pttProject

@szhu07
Copy link
Author

szhu07 commented Feb 21, 2025

Image

Image
Image
Image
Image

These are images of the setup!

@szhu07
Copy link
Author

szhu07 commented Feb 22, 2025

@biomurph

This is what it shows when I run the code for PTT visualizing in Processing.

Image

@biomurph
Copy link
Contributor

@szhu07
Please make sure that you are modifying our example sketch so that the data is formatted correctly for the PulseSensor Visualizer.

Look for a line of code that says
const int OUTPUT_TYPE =
and make sure it says
const int OUTPUT_TYPE = PROCESSING_VISUALIZER;

and make note of the comments in that area of the code.
That will format the data so that it is readable by the visualizer.

I do note that you are using legit PulseSensor hardware. Thank you for supporting the original makers of PulseSensor.

@szhu07
Copy link
Author

szhu07 commented Feb 25, 2025

@biomurph

Hi! I just checked, it does say that. However when I do serial plotter it still doesn't show anything.

Image

@biomurph
Copy link
Contributor

@szhu07
That is strange.
OK instead of using the Processing Visualizer, change the OUTPUT_TYPE back to SERIAL_PLOTTER and run the Arduino Serial Plotter to visualize the PulseSensor data?

@szhu07
Copy link
Author

szhu07 commented Feb 25, 2025

@biomurph

Also! When I open the file for Processing that I downloaded from the repo, I couldn't find a line that says
const int OUTPUT_TYPE = PROCESSING_VISUALIZER; or just const int OUTPUT_TYPE in the code. On Processing, I open the file that I download from the repo, correct?

@biomurph
Copy link
Contributor

@szhu07
We have two ways to visualize the heartbeat data

Arduino Serial Plotter
In your Arduino Sketch, make sure that the OUTPUT_TYPE = SERIAL_PLOTTER
Then, program the board and open the Arduino Serial Plotter, and you should see PulseSensor data

PulseSensor Processing Visualizer
In your Arduino Sketch, make sure that the OUTPUT_TYPE = PROCESSING_VISUALIZER
Then, open the Processing sketch of your choice from our repo. You are exploring PTT, so when you open https://github.com/szhu07/pttProject you will be given a prompt to connect to your arduino board. Use the same board that you used to program it. If you have Arduino IDE open, make sure you close all serial ports, or else the Processing Sketch won’t be able to find the board.

@szhu07
Copy link
Author

szhu07 commented Feb 26, 2025

@biomurph

This is what I am getting in Arduino serial plotter

Image

However, I'm not seeing a line with OUTPUT_TYPE in Processing that I downloaded for the PTT repo from you guys. Below is the code that I downloaded from your repo.

`/*
DISPLAYS MULTIPLE PULSE SENSOR DATA STREAMS
THIS PROGRAM WORKS WITH PulseSensorAmped_n_Sensors ARDUINO CODE

PRESS 'S' OR 's' KEY TO SAVE A PICTURE OF THE SCREEN IN SKETCH FOLDER (.jpg)
PRESS 'R' OR 'r' KEY TO RESET THE DATA TRACES
MADE BY JOEL MURPHY WINTER 2016, MODIFIED WINTER 2017
UPDATED BY JOEL MURPHY SUMMER 2016 WITH SERIAL PORT LOCATOR TOOL
UPDATED BY JOEL MURPHY WINTER 2017 WITH IMPROVED SERIAL PORT SELECTOR TOOL

THIS CODE PROVIDED AS IS, WITH NO CLAIMS OF FUNCTIONALITY OR EVEN IF IT WILL WORK
WYSIWYG
THIS CODE SUBJECT TO CHANGE WITHOUT NOTICE
*/

import processing.serial.*;
PFont font;

Serial port;
int numSensors = 2;

int[] Sensor; // HOLDS PULSE SENSOR DATA FROM ARDUINO
int[] IBI; // HOLDS TIME BETWEN HEARTBEATS FROM ARDUINO
int[] BPM; // HOLDS HEART RATE VALUE FROM ARDUINO
int[][] RawPPG; // HOLDS HEARTBEAT WAVEFORM DATA BEFORE SCALING
int[][] ScaledPPG; // USED TO POSITION SCALED HEARTBEAT WAVEFORM
int[][] ScaledBPM; // USED TO POSITION BPM DATA WAVEFORM
float offset; // USED WHEN SCALING PULSE WAVEFORM TO PULSE WINDOW
color eggshell = color(255, 253, 248);
int heart[]; // USED TO TIME THE HEART 'PULSE'
int PTT;

// THESE VARIABLES DETERMINE THE SIZE OF THE DATA WINDOWS
int PulseWindowWidth; // = 490;
int PulseWindowHeight; // = 512;
int PulseWindowX;
int PulseWindowY[];
int BPMWindowWidth; // = 180;
int BPMWindowHeight; // = 340;
int BPMWindowX;
int BPMWindowY[];
int spacer = 10;
boolean beat[]; // set when a heart beat is detected, then cleared when the BPM graph is advanced

// SERIAL PORT STUFF TO HELP YOU FIND THE CORRECT SERIAL PORT
String serialPort;
String[] serialPorts = new String[Serial.list().length];
boolean serialPortFound = false;
Radio[] button = new Radio[Serial.list().length*2];
int numPorts = serialPorts.length;
boolean refreshPorts = false;

void setup() {
size(900, 725); // Stage size
frameRate(50);
font = loadFont("Arial-BoldMT-24.vlw");
textFont(font);
textAlign(CENTER);
rectMode(CORNER);
ellipseMode(CENTER);
// Display Window Setup
PulseWindowWidth = 490;
PulseWindowHeight = 640/numSensors;
PulseWindowX = 10;
PulseWindowY = new int [numSensors];
for(int i=0; i<numSensors; i++){
PulseWindowY[i] = 43 + (PulseWindowHeight * i);
if(i > 0) PulseWindowY[i]+=spaceri;
}
BPMWindowWidth = 180;
BPMWindowHeight = PulseWindowHeight;
BPMWindowX = PulseWindowX + PulseWindowWidth + 10;
BPMWindowY = new int [numSensors];
for(int i=0; i<numSensors; i++){
BPMWindowY[i] = 43 + (BPMWindowHeight * i);
if(i > 0) BPMWindowY[i]+=spacer
i;
}
heart = new int[numSensors];
beat = new boolean[numSensors];
// Data Variables Setup
Sensor = new int[numSensors]; // HOLDS PULSE SENSOR DATA FROM ARDUINO
IBI = new int[numSensors]; // HOLDS TIME BETWEN HEARTBEATS FROM ARDUINO
BPM = new int[numSensors]; // HOLDS HEART RATE VALUE FROM ARDUINO
RawPPG = new int[numSensors][PulseWindowWidth]; // initialize raw pulse waveform array
ScaledPPG = new int[numSensors][PulseWindowWidth]; // initialize scaled pulse waveform array
ScaledBPM = new int [numSensors][BPMWindowWidth]; // initialize BPM waveform array

// set the visualizer lines to 0
resetDataTraces();

background(0);
noStroke();
// DRAW OUT THE PULSE WINDOW AND BPM WINDOW RECTANGLES
drawDataWindows();
drawHeart();

// GO FIND THE ARDUINO
fill(eggshell);
text("Select Your Serial Port",245,30);
listAvailablePorts();
}

void draw() {
if(serialPortFound){
// ONLY RUN THE VISUALIZER AFTER THE PORT IS CONNECTED
background(0);
drawDataWindows();
drawPulseWaveform();
drawBPMwaveform();
drawHeart();
printDataToScreen();

} else { // SCAN TO FIND THE SERIAL PORT
autoScanPorts();

if(refreshPorts){
refreshPorts = false;
drawDataWindows();
drawHeart();
listAvailablePorts();
}

for(int i=0; i<numPorts+1; i++){
button[i].overRadio(mouseX,mouseY);
button[i].displayRadio();
}

}

} //end of draw loop

void drawDataWindows(){
noStroke();
// DRAW OUT THE PULSE WINDOW AND BPM WINDOW RECTANGLES
fill(eggshell); // color for the window background
for(int i=0; i<numSensors; i++){
rect(PulseWindowX, PulseWindowY[i], PulseWindowWidth, PulseWindowHeight);
rect(BPMWindowX, BPMWindowY[i], BPMWindowWidth, BPMWindowHeight);
}
}

void drawPulseWaveform(){
// DRAW THE PULSE WAVEFORM
// prepare pulse data points
for (int i=0; i<numSensors; i++) {
RawPPG[i][PulseWindowWidth-1] = (1023 - Sensor[i]); // place the new raw datapoint at the end of the array

for (int j = 0; j < PulseWindowWidth-1; j++) {      // move the pulse waveform by
  RawPPG[i][j] = RawPPG[i][j+1];                         // shifting all raw datapoints one pixel left
  float dummy = RawPPG[i][j] * 0.625/numSensors;       // adjust the raw data to the selected scale
  offset = float(PulseWindowY[i]);                // calculate the offset needed at this window
  ScaledPPG[i][j] = int(dummy) + int(offset);   // transfer the raw data array to the scaled array
}
stroke(250, 0, 0);                               // red is a good color for the pulse waveform
noFill();
beginShape();                                  // using beginShape() renders fast
for (int x = 1; x < PulseWindowWidth-1; x++) {
  vertex(x+10, ScaledPPG[i][x]);                    //draw a line connecting the data points
}
endShape();

}

}

void drawBPMwaveform(){
// DRAW THE BPM WAVE FORM
// first, shift the BPM waveform over to fit then next data point only when a beat is found
for (int i=0; i<numSensors; i++) { // ONLY ADVANCE THE BPM WAVEFORM WHEN THERE IS A BEAT
if (beat[i] == true) { // move the heart rate line over one pixel every time the heart beats
beat[i] = false; // clear beat flag (beat flag waset in serialEvent tab)

for (int j=0; j<BPMWindowWidth-1; j++) {
  ScaledBPM[i][j] = ScaledBPM[i][j+1];                  // shift the bpm Y coordinates over one pixel to the left
}
// then limit and scale the BPM value
BPM[i] = constrain(BPM[i], 0, 200);                     // limit the highest BPM value to 200
float dummy = map(BPM[i], 0, 200, BPMWindowY[i]+BPMWindowHeight, BPMWindowY[i]);   // map it to the heart rate window Y
ScaledBPM[i][BPMWindowWidth-1] = int(dummy);       // set the rightmost pixel to the new data point value

}
}
// GRAPH THE HEART RATE WAVEFORM
stroke(250, 0, 0); // color of heart rate graph
strokeWeight(2); // thicker line is easier to read
noFill();

for (int i=0; i<numSensors; i++) {
beginShape();
for (int j=0; j < BPMWindowWidth; j++) { // variable 'j' will take the place of pixel x position
vertex(j+BPMWindowX, ScaledBPM[i][j]); // display history of heart rate datapoints
}
endShape();
}
}
void drawHeart(){
// DRAW THE HEART AND MAYBE MAKE IT BEAT
fill(250,0,0);
stroke(250,0,0);
int bezierZero = 0;
for(int i=0; i<numSensors; i++){
// the 'heart' variable is set in serialEvent when arduino sees a beat happen
heart[i]--; // heart is used to time how long the heart graphic swells when your heart beats
heart[i] = max(heart[i], 0); // don't let the heart variable go into negative numbers
if (heart[i] > 0) { // if a beat happened recently,
strokeWeight(8); // make the heart big
}
smooth(); // draw the heart with two bezier curves
bezier(width-100, bezierZero+70, width-20, bezierZero, width, bezierZero+160, width-100, bezierZero+170);
bezier(width-100, bezierZero+70, width-190, bezierZero, width-200, bezierZero+160, width-100, bezierZero+170);
strokeWeight(1); // reset the strokeWeight for next time
bezierZero += BPMWindowHeight+spacer;
}
}

void listAvailablePorts(){
println(Serial.list()); // print a list of available serial ports to the console
serialPorts = Serial.list();
fill(0);
textFont(font,16);
textAlign(LEFT);
// set a counter to list the ports backwards
int yPos = 0;

for(int i=numPorts-1; i>=0; i--){
button[i] = new Radio(35, 95+(yPos20),12,color(180),color(80),color(255),i,button);
text(serialPorts[i],50, 100+(yPos
20));
yPos++;
}
int p = numPorts;
fill(233,0,0);
button[p] = new Radio(35, 95+(yPos20),12,color(180),color(80),color(255),p,button);
text("Refresh Serial Ports List",50, 100+(yPos
20));

textFont(font);
textAlign(CENTER);
}

void autoScanPorts(){
if(Serial.list().length != numPorts){
if(Serial.list().length > numPorts){
println("New Ports Opened!");
int diff = Serial.list().length - numPorts; // was serialPorts.length
serialPorts = expand(serialPorts,diff);
numPorts = Serial.list().length;
}else if(Serial.list().length < numPorts){
println("Some Ports Closed!");
numPorts = Serial.list().length;
}
refreshPorts = true;
return;
}
}

void resetDataTraces(){
for (int i=0; i<numSensors; i++) {
BPM[i] = 0;
for(int j=0; j<BPMWindowWidth; j++){
ScaledBPM[i][j] = BPMWindowY[i] + BPMWindowHeight;
}
}
for (int i=0; i<numSensors; i++) {
Sensor[i] = 512;
for (int j=0; j<PulseWindowWidth; j++) {
RawPPG[i][j] = 1024 - Sensor[i]; // initialize the pulse window data line to V/2
}
}
}

void printDataToScreen(){ // PRINT THE DATA AND VARIABLE VALUES
fill(eggshell); // get ready to print text
text("Pulse Sensor Pulse Transit Time Visualizer", 300, 30); // tell them what you are
for (int i=0; i<numSensors; i++) {
text("Sensor # " + (i+1), 800, BPMWindowY[i] + 220);
text(BPM[i] + " BPM", 800, BPMWindowY[i] +185);// 215 // print the Beats Per Minute
text("IBI " + IBI[i] + "mS", 800, BPMWindowY[i] + 160);// 245 // print the time between heartbeats in mS
}
text("PTT " + PTT + "mS", 800, BPMWindowY[1] - 50);
}`

@biomurph
Copy link
Contributor

@szhu07
Please note that the OUTPUT_TYPE variable is only in the Arduino sketch. It is NOT in the Processing sketch.
The value of the variable tells the Arduino sketch how to format the data it sends to the computer. The Processing Sketch is on the computer, and will only accept data that is correctly formatted to it.

Thank you for sending the screenshot of the PulseSensor waveforms.
I am guessing that the red line is the PulseSensor on your fingertip, and the blue line is the PulseSensor on your wrist, correct?

The red trace is saturating the signal range at the high end, that is why you are seeing the ‘flat top’ to the waveform.
You can adjust the amount of pressure to bring the signal down so that the peak is visible within the ADC range (0-1023).

For the blue trace, try different parts of your body to see how the waveform changes. You mentioned that you are using your wrist, but try your upper forearm? other places? See where you get better or worse pulse signals?

@szhu07
Copy link
Author

szhu07 commented Feb 26, 2025

@biomurph

What's the reason for why the wrist is a bad spot?

Also do you think it's possible to create a cuffless blood pressure monitor with the two pulsesensors using PTT?

@biomurph
Copy link
Contributor

@szhu07
PPG measures the change in tissue density. When the shockwave from your heart beat travels through your body, tissues that are infused with blood will become less dense, and that is what is measured as the ‘pulse’.
Because of this, the best places on your body to get a clean PPG signal is where there is capillary tissue (fingertip, earlobe, etc). The wrist area does not have alot of capillary tissue. Muscle, bone, tendon, but not much capillary tissue.

Please try the PulseSensor on different parts of your body to see where you get good signal?

Your idea to create a cuffless blood pressure monitor is a known thing, however it will not give you an absolute BP measurement. It will only give you relative change in arterial stiffness. There is lots of info online (science publications, white papers) that go into this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants