Heart Rate Data with the Pulse Sensor and MetaWear

unnamed

INTRODUCTION

Heart rate (HR) data can be extremely useful when designing a fitness tracker or any sort of device that might use HR data as input. Whether you are designing jewelry that measures your stress levels before a test or a Tshirt that blinks as quickly as you dance at the club, you need HR data.

Generally, heart rate is difficult to measure but the Pulse Sensor, an open source Kickstarter project, is a simple IR based sensor design that we can use to measure heart rate. IR based heart rate sensors are fairly reliable as long as they are properly placed on the body, however, I generally prefer dry electrode based HR sensor designs.

IMG_2146

Electrode based HR sensors, specifically dry electrodes, consumes less power, give a more reliable heart rate reading from the wrist and do not necessarily need direct skin contact (as apposed to their IR counterpart).

This project from Oregon State is worth a read to learn more about the types of HR sensors and technology.

The Pulse Sensor is a plug-and-play heart-rate sensor. It can be used by anyone who wants to easily incorporate live heart-rate data into their projects. It combines a simple optical heart rate sensor with amplification and noise cancellation circuitry making it fast and easy to get reliable pulse readings. The great thing about the Pulse Sensor is that all of the analog circuitry has already been done and tested for you making the digital integration of the sensor into your product effortless.

The Pulse Sensor uses just 4mA of current draw at 5V so it is great for low-power mobile applications.

Simply clip the Pulse Sensor to your finger tip and plug it into your 3 Volts line on the MetaWear and you’re ready to read heart rate!

CIRCUIT

Our simple circuit setup is as follows:

  • PULSE SENSOR RED wire = +3V to +5V -> attached to METAWEAR 3V PIN (pin 11)
  • PULSE SENSOR BLACK wire = GND -> attached to METAWEAR GND PIN (pin 4)
  • PULSE SENSOR PURPLE wire = Signal -> attached to METAWEAR GPIO0 (pin 8)

IMG_2149

I hacked the MetaWear sample IOS App to display heart rate data on the accelerometer graph. Here is a sample:

IMG_2150

Here are some of my output logs for the XCODE debugger:

Screen Shot 2014-11-10 at 8.36.07 PM

We can see expected IBI values (in the range of 700mS to 900mS) which are stored in an array and then averaged. We can also see the peaks and throughs we are constantly calculating. Heart rate is calculated from IBI as such:

HR = 60000 / IBI

In our case, the patient in the XCODE snapshot has a Heart Rate of 81; this is an expected value for someone at rest.

There is more information about calculating HR here or you can simply Google IBI and HR for more information about these algorithms.

APPLICATION

I took the code from the Pulse Sensor Arduino sketch and translated it to Objective-C (these are the algorithm used to go from 10 averaged IBI values to HR data).

I then hacked the accelerometer function in the sample iOS App for MetaWear (code here) such that when startAccelerationPressed is called, it triggers a periodic timer that reads the Analog Value of GPIO0 (which is attached to the data pin of the Pulse Sensor).

Every 0.05 seconds the updateGPIOAnalogRead function is called:

- (IBAction)startAccelerationPressed:(id)sender {
    self.accelerometerGraph.fullScale = 2;
    [self.startAccelerometer setEnabled:FALSE];
    [self.stopAccelerometer setEnabled:TRUE];
    self.accelerometerRunning = YES;

    // These variables are used for data recording
    self.accelerometerDataArray = [[NSMutableArray alloc] initWithCapacity:1000000];
    self.readAnalogPinTimer = [NSTimer timerWithTimeInterval:0.05f target:self selector:@selector(updateGPIOAnalogRead) userInfo:nil repeats:YES];
    [[NSRunLoop mainRunLoop] addTimer:self.readAnalogPinTimer forMode:NSRunLoopCommonModes];
}

Here is the code for the periodic function updateGPIOAnalogRead:

- (void)updateGPIOAnalogRead {
    [self.device.gpio readAnalogPin:0 mode:MBLAnalogReadModeSupplyRatio handler:^(NSDecimalNumber *analogNumber, NSError *error) {
        [self.accelerometerGraph addX:[analogNumber floatValue] y:0 z:0];

        // Add data to data array for saving
        [self.accelerometerDataArray addObject:analogNumber];

        int Signal = [analogNumber floatValue]*512;

        // We take a reading every 0.05 seconds
        sampleCounter += 50;                        // keep track of the time in mS with this variable
        int N = sampleCounter – lastBeatTime;       // monitor the time since the last beat to avoid noise

        // Find the peak and trough of the pulse wave
        if(Signal < thresh && N > (IBI/5)*3){       // avoid dichrotic noise by waiting 3/5 of last IBI
            if (Signal < T){                        // T is the trough
                T = Signal;                         // keep track of lowest point in pulse wave
            }
        }

        if(Signal > thresh && Signal > P){          // thresh condition helps avoid noise
            P = Signal;                             // P is the peak
        }                                           // keep track of highest point in pulse wave

        // Look for the heart beat
        if (N > 250){                               // avoid high frequency noise
            if ( (Signal > thresh) && (Pulse == false) && (N > (IBI/5)*3) ){
                Pulse = true;                       // set the Pulse flag when we think there is a pulse
                IBI = sampleCounter – lastBeatTime; // measure time between beats in mS
                lastBeatTime = sampleCounter;       // keep track of time for next pulse

                if(secondBeat){                     // if this is the second beat, if secondBeat == TRUE
                    secondBeat = FALSE;             // clear secondBeat flag
                    for(int i=0; i<=9; i++){        // seed the running total to get a realisitic BPM at startup
                        [rate insertObject:[NSNumber numberWithInt:IBI] atIndex:i];
                    }
                }

                if(firstBeat){                      // if it’s the first time we found a beat, if firstBeat == TRUE
                    NSLog(@”First beat”);
                    firstBeat = FALSE;              // clear firstBeat flag
                    secondBeat = TRUE;              // set the second beat flag
                    return;                         // IBI value is unreliable so discard it
                }

                // Keep a running total of the last 10 IBI values
                int runningTotal = 0;               // clear the runningTotal variable
                for(int i=0; i<=8; i++){            // shift data in the rate array
                    [rate replaceObjectAtIndex:i withObject:[rate objectAtIndex:i+1]]; // and drop the oldest IBI value
                    runningTotal += (int)[[rate objectAtIndex:i] integerValue]; // add up the 9 oldest IBI values
                }

                [rate removeObjectAtIndex:9];
                [rate insertObject:[NSNumber numberWithInt:IBI] atIndex:9]; // add the latest IBI to the rate array
                runningTotal += (int)[[rate objectAtIndex:9] integerValue]; // add the latest IBI to runningTotal
                runningTotal /= 10;                 // average the last 10 IBI values
                BPM = 60000/runningTotal;           // get the beats per minutes -> BPM
            }
        }

        if (Signal < thresh && Pulse == TRUE){      // when the values are going down, the beat is over
            Pulse = FALSE;                          // reset the Pulse flag so we can do it again
            amp = P – T;                            // get amplitude of the pulse wave
            thresh = amp/2 + T;                     // set thresh at 50% of the amplitude
            P = thresh;                             // reset these for next time
            T = thresh;
        }

        if (N > 2500){                              // if 2.5 seconds go by without a beat -> reset
            thresh = 250;                           // set thresh default
            P = 250;                                // set P default
            T = 250;                                // set T default
            lastBeatTime = sampleCounter;           // bring the lastBeatTime up to date
            firstBeat = true;                       // set these to avoid noise
            secondBeat = false;                     // when we get the heartbeat back
        }
    }];
}

NEXT STEPS

There are many improvements to make from here such as tying the ground pin to a GPIO pin and setting it low (0V) a few milliseconds before the Analog read on the data pin of the Pulse Sensor. With the current setup the Pulse Sensor is always on and thus always drawing power; as such the MetaWear will run out of batteries after just a few days.