code.py
1 # SPDX-FileCopyrightText: 2020 Jeff Epler for Adafruit Industries 2 # 3 # SPDX-License-Identifier: MIT 4 5 import time 6 7 import adafruit_apds9960.apds9960 8 import board 9 import digitalio 10 from ulab import numpy as np 11 12 # Blank the screen. Scrolling text causes unwanted delays. 13 import displayio 14 d = displayio.Group() 15 board.DISPLAY.show(d) 16 17 # Filter computed at https://fiiir.com/ 18 # Sampling rate: 8Hz 19 # Cutoff freqency: 0.5Hz 20 # Transition bandwidth 0.25Hz 21 # Window type: Regular 22 # Number of coefficients: 31 23 # Manually trimmed to 16 coefficients 24 taps = np.array([ 25 +0.861745279666917052/2, 26 -0.134728583242092248, 27 -0.124472980501612152, 28 -0.108421190967457198, 29 -0.088015688587190874, 30 -0.065052714580474319, 31 -0.041490993500537393, 32 -0.019246940463156042, 33 -0.000000000000000005, 34 +0.014969842582454691, 35 +0.024894596100322432, 36 +0.029569415718397409, 37 +0.029338562862396955, 38 +0.025020274838643962, 39 +0.017781854357373172, 40 +0.008981905549472832, 41 ]) 42 43 # How much reflected light is required before pulse sensor activates 44 # These values are triggered when I bring my finger within a half inch. 45 # The sensor works when the finger is pressed lightly against the sensor. 46 PROXIMITY_THRESHOLD_HI = 225 47 PROXIMITY_THRESHOLD_LO = 215 48 49 # These constants control how much the sensor amplifies received light 50 APDS9660_AGAIN_1X = 0 51 APDS9660_AGAIN_4X = 1 52 APDS9660_AGAIN_16X = 2 53 APDS9660_AGAIN_64X = 3 54 55 # How often we are going to poll the sensor (If you change this, you need 56 # to change the filter above and the integration time below) 57 dt = 125000000 # 8Hz, 125ms 58 59 # Wait until after deadline_ns has passed 60 def sleep_deadline(deadline_ns): 61 while time.monotonic_ns() < deadline_ns: 62 pass 63 64 # Compute a high resolution crossing-time estimate for the sample, using a 65 # linear model 66 def estimated_cross_time(y0, y1, t0): 67 m = (y1 - y0) / dt 68 return t0 + round(-y1 / m) 69 70 i2c = board.I2C() 71 sensor = adafruit_apds9960.apds9960.APDS9960(i2c) 72 white_leds = digitalio.DigitalInOut(board.WHITE_LEDS) 73 white_leds.switch_to_output(False) 74 75 def main(): 76 sensor.enable_proximity = True 77 while True: 78 # Wait for user to put finger over sensor 79 while sensor.proximity < PROXIMITY_THRESHOLD_HI: 80 time.sleep(.01) 81 82 # After the finger is sensed, set up the color sensor 83 sensor.enable_color = True 84 # This sensor integration time is just a little bit shorter than 125ms, 85 # so we should always have a fresh value when we ask for it, without 86 # checking if a value is available. 87 sensor.integration_time = 220 88 # In my testing, 64X gain saturated the sensor, so this is the biggest 89 # gain value that works properly. 90 sensor.color_gain = APDS9660_AGAIN_4X 91 white_leds.value = True 92 93 # And our data structures 94 # The most recent data samples, equal in number to the filter taps 95 data = np.zeros(len(taps)) 96 # The filtered value on the previous iteration 97 old_value = 1 98 # The times of the most recent pulses registered. Increasing this number 99 # makes the estimation more accurate, but at the expense of taking longer 100 # before a pulse number can be computed 101 pulse_times = [] 102 # The estimated heart rate based on the recent pulse times 103 rate = None 104 # the number of samples taken 105 n = 0 106 107 # Rather than sleeping for a fixed duration, we compute a deadline 108 # in nanoseconds and wait for the new deadline time to arrive. This 109 # helps the long term frequency of measurements better match the desired 110 # frequency. 111 t0 = deadline = time.monotonic_ns() 112 # As long as their finger is over the sensor, capture data 113 while sensor.proximity >= PROXIMITY_THRESHOLD_LO: 114 deadline += dt 115 sleep_deadline(deadline) 116 value = sum(sensor.color_data) # Combination of all channels 117 data = np.roll(data, 1) 118 data[-1] = value 119 # Compute the new filtered variable by applying the filter to the 120 # recent data samples 121 filtered = np.sum(data * taps) 122 123 # We gathered enough data to fill the filters, and 124 # the light value crossed the zero line in the positive direction 125 # Therefore we need to record a pulse 126 if n > len(taps) and old_value < 0 <= filtered: 127 # This crossing time is estimated, but it increases the pulse 128 # estimate resolution quite a bit. If only the nearest 1/8s 129 # was used for pulse estimation, the smallest pulse increment 130 # that can be measured is 7.5bpm. 131 cross = estimated_cross_time(old_value, filtered, deadline) 132 # store this pulse time (in seconds since sensor-touch) 133 pulse_times.append((cross - t0) * 1e-9) 134 # and maybe delete an old pulse time 135 del pulse_times[:-10] 136 # And compute a rate based on the last recorded pulse times 137 if len(pulse_times) > 1: 138 rate = 60/(pulse_times[-1]-pulse_times[0])*(len(pulse_times)-1) 139 old_value = filtered 140 141 # We gathered enough data to fill the filters, so report the light 142 # value and possibly the estimated pulse rate 143 if n > len(taps): 144 print((filtered, rate)) 145 n += 1 146 147 # Turn off the sensor and the LED and go back to the top for another run 148 sensor.enable_color = False 149 white_leds.value = False 150 print() 151 main()