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()