Grove - Ear-clip Heart Rate Sensor Python

Hi All,

I start a new project and I am using the Grove - Ear-clip Heart Rate Sensor.
https://wiki.seeedstudio.com/Grove-Ear-clip_Heart_Rate_Sensor/

In the wiki I found only an Arduino example. Is there a python example? I use GrovePI, Raspberry 3B

Thanks
Alessandro

1 Like

Hi

I try to convert the example in ARDUINO to Python for Raspberry and GrovePI
The code that I used is :
https://wiki.seeedstudio.com/Grove-Ear-clip_Heart_Rate_Sensor/
The result is :

import time
import grovepi

# Connect the Grove Ear Clip Sensor to digital port D2
sensorPin = 3
start_time = time.time()
counter = 0
temp = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
sub = 0
data_effect = True
heart_rate = 0

max_heartpulse_duty = 2000
    
print("Please place the sensor correctly")
time.sleep(4)
print("Starting measurement...")


def millis():                            #MILLIS FUNCTION
    print("Millis")
    return (time.time() - start_time)


def arrayInit():                          #ARRAY INIT
    print("arrayInit")
    i = 0
    temp = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
    temp[20] = millis()

def sum():                               #SUM
    print("sum")
    global heart_rate
    if data_effect:
        heart_rate=1200000/(temp[20]-temp[0]);
        print  heart_rate 
    
def interrupt():
    global counter
    global temp
    global data_effect
    global sub
    
    print("Interrupt")
    print(counter)
    temp[counter] = millis()

    if counter == 0:       
        sub = temp[counter]-temp[20]
    else:
        sub = temp[counter]-temp[counter-1]

    if sub > max_heartpulse_duty:
        data_effect = False
        counter = 0
        arrayInit()

    if counter == 20 and data_effect:
        counter = 0
        sum()
    elif counter != 20 and data_effect:
        counter += 1
    else: 
        counter = 0
        data_effect = True

def main():
    arrayInit()
    grovepi.set_pin_interrupt(sensorPin, grovepi.COUNT_CHANGES, grovepi.RISING, 1000)

    value = False
    
    while True:
        value = grovepi.read_interrupt_state(sensorPin)
        if value == True:
            interrupt()
        time.sleep(.5)
        print("Loop")
     
if __name__ == '__main__':
    main()

the problem is that I have unlogical value as heart_rate. Like 83460.64433
according to me there is somenthing wrong…

1 Like

@alessandro.graps

Please edit your post and put three back-quotes ( ``` ) at top and bottom of listing

(and just at first glance - that time.sleep(.5) needs to be less than twice the maximum heart pulse - say 120 beats per second * 2 = 240 samples per second or 1/240th second or 0.004

and at second glance - never leave a print statement in an interrupt routine - once to see if it gets called perhaps but otherwise - interrupt handlers must be super quick.

and at third - the setting of start time is in a bad place.)

2 Likes

Hi @cyclicalobsessive

thanks a lot for the feedback. I tried to update the code with your suggestions.
Now I change the time

time.sleep(0.004)

and I remove the print in all methods.

I move the

start_time = time.time() 

before the while. Is it correct? or do i need the put in another place?
Now the application is quick and the result for heart_rate is for example 5554354.72215

Do you have other idea?

thanks
Alessandro

2 Likes

In your code you are setting sensorPin to 3, are you plugged into D3?

BTW, this code should be in main() just before the while loop:

print("Please place the sensor correctly")
time.sleep(4)
print("Starting measurement...")

millis() either should pass in the start_time, or it needs a “global start_time” - probably the global is the most convenient - see comment about global temp in arrayInit() below.

What is the “i=0” line for in arrayInit()? (perhaps that should be counter = 0?)

A “python way” to create a list with 21 items all 0 is:

temp = [0] * 21

arrayInit() needs to “global temp” ( globals are to be avoided in usual “python style”, but at this point you are not trying to learn Python so much as get something working, so stick with using globals. In general calling a method to modify a global (called a “side effect”) is not good style, but again you can learn style later.)

sum() is using temp but needs temp added to the globals: “global heart_rate, temp”

I’m sorry, but the interrupt() method is more involved than I can think about right now.

1 Like

Hi,

thanks again. I changed the code but I have the same issue…

import time
import grovepi

# Connect the Grove Ear Clip Sensor to digital port D3
sensorPin = 3
start_time = time.time()
counter = 0
temp = [0] * 21
sub = 0
data_effect = True
heart_rate = 0
max_heartpulse_duty = 2000
    
def millis():                            
    global start_time
    return (time.time() - start_time)

def arrayInit():  
    global temp
    global counter
    counter = 0
    temp = [0] * 21
    temp[20] = millis()

def sum():                               #SUM
    global heart_rate, temp
    if data_effect:
        heart_rate=1200000/(temp[20]-temp[0]);
    
def interrupt():
    global counter
    global temp
    global data_effect
    global sub 
    
    temp[counter] = millis()
    
    if counter == 0:       
        sub = temp[counter]-temp[20]
    else:
        sub = temp[counter]-temp[counter-1]

    if sub > max_heartpulse_duty:
        data_effect = False
        counter = 0
        arrayInit()
    if counter == 20 and data_effect:
        counter = 0
        sum()
    elif counter != 20 and data_effect:
        counter += 1
    else: 
        counter = 0
        data_effect = True

def main():
    print("Please place the sensor correctly")
    time.sleep(4)
    print("Starting measurement...")
    arrayInit()
    grovepi.set_pin_interrupt(sensorPin, grovepi.COUNT_CHANGES, grovepi.RISING, 1000)

    value = False
    
    while True:
        value = grovepi.read_interrupt_state(sensorPin)
        if value == True:
            interrupt()
        
        print(heart_rate)
        print("Loop")
     
 
if __name__ == '__main__':
    main()

1 Like
  1. Please change value == True to
    value > 0
    It doesn’t need this change to work for heart rates under 120 bpm, but people should not treat integers as booleans.

  2. Sum() needs global data_effect.

  3. Main() needs global heart_rate

  4. Not that it hurts anything, but remove these unneeded lines:

  • sub does not need to be initialized (sub = 0)
  • sub does not need to be global (global sub)
  1. xxxx I’m going crazy

  2. put that time.sleep(0.5) back in and

  3. change the print statement to only print two digits

  4. get rid of the Loop print statement

so the loop in main() should look like:

while True:
        value = grovepi.read_interrupt_state(sensorPin)
        if value == True:
            interrupt()
        time.sleep(0.5)
        print("HR: {:2.0f}".format(heart_rate))
        

Description of process in words:

If I understand correctly, “value” from read_interrupt_state() will usually be the number of pulses in 1 second, which will be 0 or 1 for pulse less than 120 beats per minute.

The temp array is initialized with temp[20] set to the number of ms since the start time and the count of pulses starts as 0.

Every time the check of the interrupt pin pulse count indicates that a pulse has occurred, the milliseconds since start time is stored in temp[count].

Then the time since last pulse is computed. If the time since the last pulse is greater than 2 seconds the temp array gets reset, and the data effect flag is set to false to prevent further processing.

When there are 21 pulses seen, the HR gets calculated from the difference between the new temp[20] and the first pulse temp[0]. (last_ms_since_start_time - first_ms_since_start_time) = ms between 21st and first pulse = time for 20 pulses. HR = 1200000/time_in_ms_for_20_pulses

example: for 60 beats per minute -

  • time between each pulse is 1s * 1000 = 1000 ms
  • time between first pulse [count 0] and 21st pulse [count 20] will be (20 x 1000)=20000 ms
  • HR = 1200000 / 20000 = 60

The loop is checking twice per second for the expected, roughly once per second pulse.

** Please post what your whole code looks like after you have made the changes (remember to enclose with ``` at start and end) **

After you are seeing a good HR from your program, you could take a look at all the places where counter gets set to 0. I think there is at least once that is not needed, it probably doesn’t hurt anything, but it also shouldn’t be there as it will confuse anyone trying to figure out your code later.

@alessandro.graps sorry for so many edits on that last reply - ignore the version in the email notice of reply. Check the forum online for the real thing.

Hi @cyclicalobsessive

thanks a lot for your help. You don’t worry for the many edits reply anz thanks a gain for your time and for your help.

I updated the code. Here now you can see the new code:

REMOVE THE SUB INITIALIZATION

[..]
data_effect = True
heart_rate = 0
max_heartpulse_duty = 2000
[..]

ADD DATA_EFFECT IN GLOBAL IN SUM

def sum():                               
    global heart_rate, temp, data_effect
    if data_effect:
        heart_rate=1200000/(temp[20]-temp[0]);

#REMOVE SUB AS GLOBAL

def interrupt():
    global counter
    global temp
    global data_effect
    
    temp[counter] = millis()
    
    if counter == 0:       
        sub = temp[counter]-temp[20]
    else:
        sub = temp[counter]-temp[counter-1]

    if sub > max_heartpulse_duty:
        data_effect = False
        counter = 0
        arrayInit()
    if counter == 20 and data_effect:
        counter = 0
        sum()
    elif counter != 20 and data_effect:
        counter += 1
    else: 
        counter = 0
        data_effect = True

CHANGE THE MAIN

def main():

    global heart_rate
    
    print("Please place the sensor correctly")
    time.sleep(4)
    print("Starting measurement...")
    arrayInit()
    grovepi.set_pin_interrupt(sensorPin, grovepi.COUNT_CHANGES, grovepi.RISING, 1000)

    while True:
        value = grovepi.read_interrupt_state(sensorPin)
        if value > 0:
            interrupt()
        time.sleep(0.5)
        print("HR: {:2.0f}".format(heart_rate))
     

Now the result is the following:

They are strange value…

1 Like

Could you try:

  • add the imports listed in this fragment below
  • change the initialization of start_time to be like the fragment
  • change the definition of millis() to be like the fragment below
from datetime import datetime
from datetime import timedelta

start_time = datetime.now()

# returns the elapsed milliseconds since the start of the program
def millis():
   global start_time
   dt = datetime.now() - start_time
   ms = (dt.days * 24 * 60 * 60 + dt.seconds) * 1000 + dt.microseconds / 1000.0
   return ms

Hi @cyclicalobsessive

thanks again. I updated the code and it is working. The unique change that I did is on time.sleep also.

Here the complete code:

from datetime import datetime

from datetime import timedelta
import time
import grovepi

# Connect the Grove Ear Clip Sensor to digital port D3
sensorPin = 3
start_time = datetime.now()
counter = 0
temp = [0] * 21
data_effect = True
heart_rate = 0
max_heartpulse_duty = 2000
    
def millis():
   global start_time
   dt = datetime.now() - start_time
   ms = (dt.days * 24 * 60 * 60 + dt.seconds) * 1000 + dt.microseconds / 1000.0

   return ms

def arrayInit():  
    global temp
    global counter
    counter = 0
    temp = [0] * 21
    temp[20] = millis()

def sum():                               
    global heart_rate, temp, data_effect
    if data_effect:
        heart_rate=1200000/(temp[20]-temp[0]);
    
def interrupt():
    global counter
    global temp
    global data_effect
    
    temp[counter] = millis()
    
    if counter == 0:       
        sub = temp[counter]-temp[20]
    else:
        sub = temp[counter]-temp[counter-1]

    if sub > max_heartpulse_duty:
        data_effect = False
        counter = 0
        arrayInit()
    if counter == 20 and data_effect:
        counter = 0
        sum()
    elif counter != 20 and data_effect:
        counter += 1
    else: 
        counter = 0
        data_effect = True

def main():

    global heart_rate
    
    print("Please place the sensor correctly")
    time.sleep(4)
    print("Starting measurement...")
    arrayInit()
    grovepi.set_pin_interrupt(sensorPin, grovepi.COUNT_CHANGES, grovepi.RISING, 1000)





    while True:
        value = grovepi.read_interrupt_state(sensorPin)
        if value > 0:
            interrupt()
        time.sleep(0.73)
        print("HR: {:2.0f}".format(heart_rate))
     
 
if __name__ == '__main__':
    main()

And here the result

I make some test with a professional device and I have the same result (between 79 and 82)

What do you think?

Thanks a lot for your help
Alessandro

1 Like

WOW! You did it!

There have been others here, and on the Internet, asking for a Raspberry Pi / Python example for that sensor, for quite some time, and you created it.

Be sure to post this link to your answer over on the SeedStudio forum, and any other social platform you participate in (tag #RaspberryPi and #GrovePi also):


https://forum.dexterindustries.com/t/grove-ear-clip-heart-rate-sensor-python/7607/11?u=alessandro.graps

Congratulations, Alessandro. That is so cool!

Hi @cyclicalobsessive thanks a lot for your help. I will update the GrovePI example libraries and seedstudio example library…

Alessandro

1 Like

This thread warms my heart! So nice to see this level of collaboration! :heart_eyes::star_struck:

2 Likes