#----------------------------------------------------------------------------------
# qwiic_max3010x.py
#
# Python library for the MAX3010x Particle Sensor
#
#----------------------------------------------------------------------------------
#
# Written by SparkFun Electronics, May 2020
# Original Arduino Library written by Peter Jansen and Nathan Seidle (SparkFun)
# BSD license, all atribution text must be included in any redistribution.
#
# These sensors use I2C to communicate, as well as a single (optional)
# interrupt line that is not currently supported in this driver.
#
# This python library supports the SparkFun Electroncis qwiic
# qwiic sensor/board ecosystem
#
# More information on qwiic is at https:// www.sparkfun.com/qwiic
#
# Do you like this library? Help support SparkFun. Buy a board!
#
# This sensor can easily protyped using the following:
# SparkFun Particle Sensor Breakout - MAX30101 (Qwiic)
# https://www.sparkfun.com/products/16474
#
#==================================================================================
# Copyright (c) 2020 SparkFun Electronics
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#==================================================================================
#
# This is mostly a port of existing Arduino functionaly, so pylint is sad.
# The goal is to keep the public interface pthonic, but internal is internal
#
# pylint: disable=line-too-long, too-many-public-methods, invalid-name
#
"""
qwiic_max3010x
============
Python module for the MAX3010x sensor as found on the [SparkFun Particle Sensor Breakout - MAX30101 (Qwiic)](https://www.sparkfun.com/products/16474)
This python package is a port of the existing [SparkFun MAX3010x Sensor Arduino Library](https://github.com/sparkfun/SparkFun_MAX3010x_Sensor_Library)
This package can be used in conjunction with the overall [SparkFun qwiic Python Package](https://github.com/sparkfun/Qwiic_Py)
New to qwiic? Take a look at the entire [SparkFun qwiic ecosystem](https://www.sparkfun.com/qwiic).
"""
#-----------------------------------------------------------------------------
from __future__ import print_function
import struct
import qwiic_i2c
import time
from smbus2 import SMBus, i2c_msg
_i2c_msg = i2c_msg
from . import heart_rate
hr = heart_rate.HeartRate()
# Define the device name and I2C addresses. These are set in the class defintion
# as class variables, making them avilable without having to create a class instance.
# This allows higher level logic to rapidly create a index of qwiic devices at
# runtine
#
# The name of this device
_DEFAULT_NAME = "Qwiic MAX3010x"
# Some devices have multiple availabel addresses - this is a list of these addresses.
# NOTE: The first address in this list is considered the default I2C address for the
# device.
_AVAILABLE_I2C_ADDRESS = [0x57] # 7-bit I2C Address
# Note that MAX30102 has the same I2C address and Part ID
# Status Registers
MAX30105_INTSTAT1 = 0x00
MAX30105_INTSTAT2 = 0x01
MAX30105_INTENABLE1 = 0x02
MAX30105_INTENABLE2 = 0x03
# FIFO Registers
MAX30105_FIFOWRITEPTR = 0x04
MAX30105_FIFOOVERFLOW = 0x05
MAX30105_FIFOREADPTR = 0x06
MAX30105_FIFODATA = 0x07
# Configuration Registers
MAX30105_FIFOCONFIG = 0x08
MAX30105_MODECONFIG = 0x09
MAX30105_PARTICLECONFIG = 0x0A # Note, sometimes listed as "SPO2" config in datasheet (pg. 11)
MAX30105_LED1_PULSEAMP = 0x0C
MAX30105_LED2_PULSEAMP = 0x0D
MAX30105_LED3_PULSEAMP = 0x0E
MAX30105_LED_PROX_AMP = 0x10
MAX30105_MULTILEDCONFIG1 = 0x11
MAX30105_MULTILEDCONFIG2 = 0x12
# Die Temperature Registers
MAX30105_DIETEMPINT = 0x1F
MAX30105_DIETEMPFRAC = 0x20
MAX30105_DIETEMPCONFIG = 0x21
# Proximity Function Registers
MAX30105_PROXINTTHRESH = 0x30
# Part ID Registers
MAX30105_REVISIONID = 0xFE
MAX30105_PARTID = 0xFF # Should always be 0x15. Identical to MAX30102.
# MAX30105 Commands
# Interrupt configuration (pg 13, 14)
MAX30105_INT_A_FULL_MASK = (~0b10000000)
MAX30105_INT_A_FULL_ENABLE = 0x80
MAX30105_INT_A_FULL_DISABLE = 0x00
MAX30105_INT_DATA_RDY_MASK = (~0b01000000)
MAX30105_INT_DATA_RDY_ENABLE = 0x40
MAX30105_INT_DATA_RDY_DISABLE = 0x00
MAX30105_INT_ALC_OVF_MASK = (~0b00100000)
MAX30105_INT_ALC_OVF_ENABLE = 0x20
MAX30105_INT_ALC_OVF_DISABLE = 0x00
MAX30105_INT_PROX_INT_MASK = (~0b00010000)
MAX30105_INT_PROX_INT_ENABLE = 0x10
MAX30105_INT_PROX_INT_DISABLE = 0x00
MAX30105_INT_DIE_TEMP_RDY_MASK = (~0b00000010)
MAX30105_INT_DIE_TEMP_RDY_ENABLE = 0x02
MAX30105_INT_DIE_TEMP_RDY_DISABLE = 0x00
MAX30105_SAMPLEAVG_MASK = (~0b11100000)
MAX30105_SAMPLEAVG_1 = 0x00
MAX30105_SAMPLEAVG_2 = 0x20
MAX30105_SAMPLEAVG_4 = 0x40
MAX30105_SAMPLEAVG_8 = 0x60
MAX30105_SAMPLEAVG_16 = 0x80
MAX30105_SAMPLEAVG_32 = 0xA0
MAX30105_ROLLOVER_MASK = 0xEF
MAX30105_ROLLOVER_ENABLE = 0x10
MAX30105_ROLLOVER_DISABLE = 0x00
MAX30105_A_FULL_MASK = 0xF0
# Mode configuration commands (page 19)
MAX30105_SHUTDOWN_MASK = 0x7F
MAX30105_SHUTDOWN = 0x80
MAX30105_WAKEUP = 0x00
MAX30105_RESET_MASK = 0xBF
MAX30105_RESET = 0x40
MAX30105_MODE_MASK = 0xF8
MAX30105_MODE_REDONLY = 0x02
MAX30105_MODE_REDIRONLY = 0x03
MAX30105_MODE_MULTILED = 0x07
# Particle sensing configuration commands (pgs 19-20)
MAX30105_ADCRANGE_MASK = 0x9F
MAX30105_ADCRANGE_2048 = 0x00
MAX30105_ADCRANGE_4096 = 0x20
MAX30105_ADCRANGE_8192 = 0x40
MAX30105_ADCRANGE_16384 = 0x60
MAX30105_SAMPLERATE_MASK = 0xE3
MAX30105_SAMPLERATE_50 = 0x00
MAX30105_SAMPLERATE_100 = 0x04
MAX30105_SAMPLERATE_200 = 0x08
MAX30105_SAMPLERATE_400 = 0x0C
MAX30105_SAMPLERATE_800 = 0x10
MAX30105_SAMPLERATE_1000 = 0x14
MAX30105_SAMPLERATE_1600 = 0x18
MAX30105_SAMPLERATE_3200 = 0x1C
MAX30105_PULSEWIDTH_MASK = 0xFC
MAX30105_PULSEWIDTH_69 = 0x00
MAX30105_PULSEWIDTH_118 = 0x01
MAX30105_PULSEWIDTH_215 = 0x02
MAX30105_PULSEWIDTH_411 = 0x03
#Multi-LED Mode configuration (pg 22)
MAX30105_SLOT1_MASK = 0xF8
MAX30105_SLOT2_MASK = 0x8F
MAX30105_SLOT3_MASK = 0xF8
MAX30105_SLOT4_MASK = 0x8F
SLOT_NONE = 0x00
SLOT_RED_LED = 0x01
SLOT_IR_LED = 0x02
SLOT_GREEN_LED = 0x03
SLOT_NONE_PILOT = 0x04
SLOT_RED_PILOT = 0x05
SLOT_IR_PILOT = 0x06
SLOT_GREEN_PILOT = 0x07
MAX_30105_EXPECTEDPARTID = 0x15
# define the class that encapsulates the device being created. All information associated with this
# device is encapsulated by this class. The device class should be the only value exported
# from this module.
[docs]class QwiicMax3010x(object):
"""
QwiicMax3010x
:param address: The I2C address to use for the device.
If not provided, the default address is used.
:param i2c_driver: An existing i2c driver object. If not provided
a driver object is created.
:return: The QwiicMax3010x device object.
:rtype: Object
"""
# Constructor
device_name = _DEFAULT_NAME
available_addresses = _AVAILABLE_I2C_ADDRESS
# activeLEDs is the number of channels turned on, and can be 1 to 3. 2 is common for Red+IR.
activeLEDs = 0 # Gets set during setup. Allows check() to calculate how many bytes to read from FIFO
# Storage size
# (This is "left over" from the arduino library, but needed for rollovers)
# Each long is 4 bytes so limit this to fit on your micro
STORAGE_SIZE = 4
# Circular buffer of readings from the sensor
red = [0,0,0,0] # place holders to make this list 4 spaces long (to mimic previous struct arrays in arduino library)
IR = [0,0,0,0]
green = [0,0,0,0]
head = 0
tail = 0
readPointer = 0
writePointer = 0
numberOfSamples = 0
# Constructor
def __init__(self, address=None, i2c_driver=None):
# Did the user specify an I2C address?
self.address = address if address is not None else self.available_addresses[0]
# load the I2C driver if one isn't provided
if i2c_driver is None:
self._i2c = qwiic_i2c.getI2CDriver()
if self._i2c is None:
print("Unable to load I2C driver for this platform.")
return
else:
self._i2c = i2c_driver
# ----------------------------------
# isConnected()
#
# Is an actual board connected to our system?
[docs] def is_connected(self):
"""
Determine if a device is conntected to the system..
:return: True if the device is connected, otherwise False.
:rtype: bool
"""
return qwiic_i2c.isDeviceConnected(self.address)
connected = property(is_connected)
# ----------------------------------
# begin()
#
# Initialize the system/validate the board.
[docs] def begin(self):
"""
Initialize the operation of the Qwiic MAX3010x module
:return: Returns true of the initializtion was successful, otherwise False.
:rtype: bool
"""
# Basically return True if we are connected...
return self.is_connected()
# ----------------------------------
# bit_mask()
#
# Given a register, read it, mask it, and then set the thing
[docs] def bit_mask(self, reg, mask, thing):
"""
Given a register, read it, mask it, and then set the thing
:param reg: the register you'd like to effect
:param mask: the mask needed to zero-out the portion of the register we're interested in
:param thing: the thing we are affecting aka the control bits of the register
:return: Returns true of the register write was successful, otherwise False.
:rtype: bool
"""
# Grab current register context, store it in local variable "temp_reg"
temp_reg = self._i2c.readByte(self.address, reg)
# Zero-out the portions of the register we're interested in
temp_reg &= mask
# Change contents
temp_reg |= thing
return self._i2c.writeByte(self.address, reg, temp_reg)
# ----------------------------------
# millis()
#
# Returns the current time in milliseconds
[docs] def millis(self):
"""
Returns the current time in milliseconds
:return: Returns current system time in milliseconds
:rtype: int32_t
"""
return int(round(time.time() * 1000))
# ----------------------------------
# softReset()
#
# Command a soft reset
[docs] def softReset(self):
"""
Command a soft reset
:return: Returns true of the soft reset was successful, otherwise False.
:rtype: bool
"""
self.bit_mask(MAX30105_MODECONFIG, MAX30105_RESET_MASK, MAX30105_RESET)
# Poll for bit to clear, reset is then complete
# Timeout after 100ms
timeout = 100
while (timeout):
response = self._i2c.readByte(self.address, MAX30105_MODECONFIG)
if ((response & MAX30105_RESET) == 0):
return True # We're done!
time.sleep(0.001) # Let's not over burden the I2C bus
timeout -= 1
return False # soft reset failure
#
# Configuration
#
# Begin Interrupt configuration
# ----------------------------------
# getINT1()
#
# Returns the value of the INTSTAT1 Register
[docs] def getINT1(self):
"""
Returns the value of the INTSTAT1 Register
:return: value of the INTSTAT1 Register
:rtype: integer
"""
return self._i2c.readByte(self.address, MAX30105_INTSTAT1)
# ----------------------------------
# getINT2()
#
# Returns the value of the INTSTAT2 Register
[docs] def getINT2(self):
"""
Returns the value of the INTSTAT2 Register
:return: value of the INTSTAT2 Register
:rtype: integer
"""
return self._i2c.readByte(self.address, MAX30105_INTSTAT2)
# ----------------------------------
# enableAFULL()
#
# Enable AFULL Interrupt
[docs] def enableAFULL(self):
"""
Enable AFULL Interrupt
:return: no return value
"""
self.bit_mask(MAX30105_INTENABLE1, MAX30105_INT_A_FULL_MASK, MAX30105_INT_A_FULL_ENABLE)
# ----------------------------------
# disableAFULL()
#
# Disable AFULL Interrupt
[docs] def disableAFULL(self):
"""
Disable AFULL Interrupt
:return: no return value
"""
self.bit_mask(MAX30105_INTENABLE1, MAX30105_INT_A_FULL_MASK, MAX30105_INT_A_FULL_DISABLE)
# ----------------------------------
# enableDATARDY()
#
# Enable DATARDY Interrupt
[docs] def enableDATARDY(self):
"""
Enable DATARDY Interrupt
:return: no return value
"""
self.bit_mask(MAX30105_INTENABLE1, MAX30105_INT_DATA_RDY_MASK, MAX30105_INT_DATA_RDY_ENABLE)
# ----------------------------------
# disableDATARDY()
#
# Disable DATARDY Interrupt
[docs] def disableDATARDY(self):
"""
Disable DATARDY Interrupt
:return: no return value
"""
self.bit_mask(MAX30105_INTENABLE1, MAX30105_INT_DATA_RDY_MASK, MAX30105_INT_DATA_RDY_DISABLE)
# ----------------------------------
# enableALCOVF()
#
# Enable ALCOVF Interrupt
[docs] def enableALCOVF(self):
"""
Enable ALCOVF Interrupt
:return: no return value
"""
self.bit_mask(MAX30105_INTENABLE1, MAX30105_INT_ALC_OVF_MASK, MAX30105_INT_ALC_OVF_ENABLE)
# ----------------------------------
# disableALCOVF()
#
# Disable ALCOVF Interrupt
[docs] def disableALCOVF(self):
"""
Disable ALCOVF Interrupt
:return: no return value
"""
self.bit_mask(MAX30105_INTENABLE1, MAX30105_INT_ALC_OVF_MASK, MAX30105_INT_ALC_OVF_DISABLE)
# ----------------------------------
# enablePROXINT()
#
# Enable PROXINT Interrupt
[docs] def enablePROXINT(self):
"""
Enable PROXINT Interrupt
:return: no return value
"""
self.bit_mask(MAX30105_INTENABLE1, MAX30105_INT_PROX_INT_MASK, MAX30105_INT_PROX_INT_ENABLE)
# ----------------------------------
# disablePROXINT()
#
# Disable PROXINT Interrupt
[docs] def disablePROXINT(self):
"""
Disable PROXINT Interrupt
:return: no return value
"""
self.bit_mask(MAX30105_INTENABLE1, MAX30105_INT_PROX_INT_MASK, MAX30105_INT_PROX_INT_DISABLE)
# ----------------------------------
# enableDIETEMPRDY()
#
# Enable DIETEMPRDY Interrupt
[docs] def enableDIETEMPRDY(self):
"""
Enable DIETEMPRDY Interrupt
:return: no return value
"""
self.bit_mask(MAX30105_INTENABLE2, MAX30105_INT_DIE_TEMP_RDY_MASK, MAX30105_INT_DIE_TEMP_RDY_ENABLE)
# ----------------------------------
# disableDIETEMPRDY()
#
# Disable DIETEMPRDY Interrupt
[docs] def disableDIETEMPRDY(self):
"""
Disable DIETEMPRDY Interrupt
:return: no return value
"""
self.bit_mask(MAX30105_INTENABLE2, MAX30105_INT_DIE_TEMP_RDY_MASK, MAX30105_INT_DIE_TEMP_RDY_DISABLE)
# End Interrupt configuration
# ----------------------------------
# shutDown()
#
# Put IC into low power mode (datasheet pg. 19)
# During shutdown the IC will continue to respond to I2C commands but will
# not update with or take new readings (such as temperature)
[docs] def shutDown(self):
"""
Put IC into low power mode
:return: no return value
"""
self.bit_mask(MAX30105_MODECONFIG, MAX30105_SHUTDOWN_MASK, MAX30105_SHUTDOWN)
# ----------------------------------
# wakeUp()
#
# Pull IC out of low power mode (datasheet pg. 19)
[docs] def wakeUp(self):
"""
Pull IC out of low power mode
:return: no return value
"""
self.bit_mask(MAX30105_MODECONFIG, MAX30105_SHUTDOWN_MASK, MAX30105_WAKEUP)
# ----------------------------------
# setLEDMode()
#
# Set which LEDs are used for sampling - Red only, RED+IR only, or custom.
# See datasheet, page 19
[docs] def setLEDMode(self, mode):
"""
Set which LEDs are used for sampling - Red only, RED+IR only, or custom
:param mode: Red only, RED+IR only, or custom
:return: no return value
"""
self.bit_mask(MAX30105_MODECONFIG, MAX30105_MODE_MASK, mode)
# ----------------------------------
# setADCRange()
#
# Set adcRange: one of MAX30105_ADCRANGE_2048, _4096, _8192, _16384
[docs] def setADCRange(self, adcRange):
"""
Set adcRange: one of MAX30105_ADCRANGE_2048, _4096, _8192, _16384
:param adcRange: MAX30105_ADCRANGE_2048, _4096, _8192, _16384
:return: no return value
"""
self.bit_mask(MAX30105_PARTICLECONFIG, MAX30105_ADCRANGE_MASK, adcRange)
# ----------------------------------
# setSampleRate()
#
# Set sampleRate: one of MAX30105_SAMPLERATE_50, _100, _200, _400, _800, _1000, _1600, _3200
[docs] def setSampleRate(self, sampleRate):
"""
Set sampleRate: one of MAX30105_SAMPLERATE_50, _100, _200, _400, _800, _1000, _1600, _3200
:param sampleRate: MAX30105_SAMPLERATE_50, _100, _200, _400, _800, _1000, _1600, _3200
:return: no return value
"""
self.bit_mask(MAX30105_PARTICLECONFIG, MAX30105_SAMPLERATE_MASK, sampleRate)
# ----------------------------------
# setPulseWidth()
#
# Set pulseWidth: one of MAX30105_PULSEWIDTH_69, _188, _215, _411
[docs] def setPulseWidth(self, pulseWidth):
"""
Set pulseWidth: one of MAX30105_PULSEWIDTH_69, _188, _215, _411
:param pulseWidth: MAX30105_PULSEWIDTH_69, _188, _215, _411
:return: no return value
"""
self.bit_mask(MAX30105_PARTICLECONFIG, MAX30105_PULSEWIDTH_MASK, pulseWidth)
# ----------------------------------
# setPulseAmplitudeRed()
#
# Set pulse amplitude (mA) of red LED
# 0x00 = 0mA, 0x7F = 25.4mA, 0xFF = 50mA (typical)
# See datasheet, page 21
[docs] def setPulseAmplitudeRed(self, amplitude):
"""
Set pulse amplitude (mA) of red LED
:param amplitude: 0x00 = 0mA, 0x7F = 25.4mA, 0xFF = 50mA (typical)
:return: no return value
"""
self._i2c.writeByte(self.address, MAX30105_LED1_PULSEAMP, amplitude)
# ----------------------------------
# setPulseAmplitudeIR()
#
# Set pulse amplitude (mA) of IR LED
# 0x00 = 0mA, 0x7F = 25.4mA, 0xFF = 50mA (typical)
# See datasheet, page 21
[docs] def setPulseAmplitudeIR(self, amplitude):
"""
Set pulse amplitude (mA) of IR LED
:param amplitude: 0x00 = 0mA, 0x7F = 25.4mA, 0xFF = 50mA (typical)
:return: no return value
"""
self._i2c.writeByte(self.address, MAX30105_LED2_PULSEAMP, amplitude)
# ----------------------------------
# setPulseAmplitudeGreen()
#
# Set pulse amplitude (mA) of green LED
# 0x00 = 0mA, 0x7F = 25.4mA, 0xFF = 50mA (typical)
# See datasheet, page 21
[docs] def setPulseAmplitudeGreen(self, amplitude):
"""
Set pulse amplitude (mA) of green LED
:param amplitude: 0x00 = 0mA, 0x7F = 25.4mA, 0xFF = 50mA (typical)
:return: no return value
"""
self._i2c.writeByte(self.address, MAX30105_LED3_PULSEAMP, amplitude)
# ----------------------------------
# setPulseAmplitudeProximity()
#
# Set pulse aplitude while in proximity mode (only MAX30105)
# Note, this is specific to the MAX30105, and not included in the MAX30101
[docs] def setPulseAmplitudeProximity(self, amplitude):
"""
Set pulse aplitude while in proximity mode (only MAX30105)
Note, this is specific to the MAX30105, and not included in the MAX30101
:param amplitude: amplitude
:return: no return value
"""
self._i2c.writeByte(self.address, MAX30105_LED_PROX_AMP, amplitude)
# ----------------------------------
# setProximityThreshold()
#
# Set the IR ADC count that will trigger the beginning of particle-sensing mode.
# The threshMSB signifies only the 8 most significant-bits of the ADC count.
# Note, this is specific to the MAX30105, and not included in the MAX30101
# See datasheet, page 24.
[docs] def setProximityThreshold(self, threshMSB):
"""
Set the IR ADC count that will trigger the beginning of particle-sensing mode.
The threshMSB signifies only the 8 most significant-bits of the ADC count.
Note, this is specific to the MAX30105, and not included in the MAX30101
:param threshMSB: threshold of ADC count to cause trigger
:return: no return value
"""
self._i2c.writeByte(self.address, MAX30105_PROXINTTHRESH, threshMSB)
#
# enableSlot()
#
# Given a slot number assign a thing to it
# Devices are SLOT_RED_LED or SLOT_RED_PILOT (proximity)
# Assigning a SLOT_RED_LED will pulse LED
# Assigning a SLOT_RED_PILOT will ??
[docs] def enableSlot(self, slotNumber, device):
"""
Given a slot number assign a thing to it
Devices are SLOT_RED_LED or SLOT_RED_PILOT (proximity)
Assigning a SLOT_RED_LED will pulse LED
Assigning a SLOT_RED_PILOT will ??
:param slotNumber: slot number as int 1,2,3,4
:param device: which device (aka led) you'd like to assign to the given slot
:return: Whether or not the configuration write was successful
:rtype: bool
"""
if slotNumber == 1:
return self.bit_mask(MAX30105_MULTILEDCONFIG1, MAX30105_SLOT1_MASK, device)
elif slotNumber == 2:
return self.bit_mask(MAX30105_MULTILEDCONFIG1, MAX30105_SLOT2_MASK, device << 4)
elif slotNumber == 3:
return self.bit_mask(MAX30105_MULTILEDCONFIG2, MAX30105_SLOT3_MASK, device)
elif slotNumber == 4:
return self.bit_mask(MAX30105_MULTILEDCONFIG2, MAX30105_SLOT4_MASK, device << 4)
else:
return False #Shouldn't be here!
#
# disableSlots()
#
# Clears all slot assignments
[docs] def disableSlots(self):
"""
Clears all slot assignments
:return: no return value
"""
self._i2c.writeByte(self.address, MAX30105_MULTILEDCONFIG1, 0)
self._i2c.writeByte(self.address, MAX30105_MULTILEDCONFIG2, 0)
#
# FIFO Configuration
#
#
# setFIFOAverage()
#
# Set sample average (Table 3, Page 18)
[docs] def setFIFOAverage(self, numberOfSamples):
"""
Set sample average
:param numberOfSamples: MAX30105_SAMPLEAVG_1, _2, _4, _8, _16, _32
:return: no return value
"""
self.bit_mask(MAX30105_FIFOCONFIG, MAX30105_SAMPLEAVG_MASK, numberOfSamples)
#
# clearFIFO()
#
# Resets all points to start in a known state
# Page 15 recommends clearing FIFO before beginning a read
[docs] def clearFIFO(self):
"""
Resets all points to start in a known state
:return: no return value
"""
self._i2c.writeByte(self.address, MAX30105_FIFOWRITEPTR, 0)
self._i2c.writeByte(self.address, MAX30105_FIFOOVERFLOW, 0)
self._i2c.writeByte(self.address, MAX30105_FIFOREADPTR, 0)
#
# enableFIFORollover()
#
# Enable roll over if FIFO over flows
[docs] def enableFIFORollover(self):
"""
Enable roll over if FIFO over flows
:return: no return value
"""
self.bit_mask(MAX30105_FIFOCONFIG, MAX30105_ROLLOVER_MASK, MAX30105_ROLLOVER_ENABLE)
#
# disableFIFORollover()
#
# Disable roll over if FIFO over flows
[docs] def disableFIFORollover(self):
"""
Disable roll over if FIFO over flows
:return: no return value
"""
self.bit_mask(MAX30105_FIFOCONFIG, MAX30105_ROLLOVER_MASK, MAX30105_ROLLOVER_DISABLE)
#
# setFIFOAlmostFull()
#
# Set number of samples to trigger the almost full interrupt (Page 18)
# Power on default is 32 samples
# Note it is reverse: 0x00 is 32 samples, 0x0F is 17 samples
[docs] def setFIFOAlmostFull(self, numberOfSamples):
"""
Set number of samples to trigger the almost full interrupt
:param numberOfSamples: default is 32 samples. Note it's reverse (0x00 is 32 samples, 0x0F is 17 samples)
:return: no return value
"""
self.bit_mask(MAX30105_FIFOCONFIG, MAX30105_A_FULL_MASK, numberOfSamples)
#
# getWritePointer()
#
# Read the FIFO Write Pointer
[docs] def getWritePointer(self):
"""
Read the FIFO Write Pointer
:return: FIFO write pointer value
:rtype: integer
"""
return self._i2c.readByte(self.address, MAX30105_FIFOWRITEPTR)
#
# getReadPointer()
#
# Read the FIFO Read Pointer
[docs] def getReadPointer(self):
"""
Read the FIFO Read Pointer
:return: FIFO read pointer value
:rtype: integer
"""
return self._i2c.readByte(self.address, MAX30105_FIFOREADPTR)
# ----------------------------------
# setup()
#
# Setup the MAX3010x with default settings
# Setup the sensor
# The MAX30105 has many settings. By default we select:
# Sample Average = 4
# Mode = MultiLED
# ADC Range = 16384 (62.5pA per LSB)
# Sample rate = 50
# Use the default setup if you are just getting started with the MAX30105 sensor
[docs] def setup(self, powerLevel = 0x1F, sampleAverage = 4, ledMode = 3, sampleRate = 400, pulseWidth = 411, adcRange = 4096):
"""
Setup the MAX3010x with default or custom settings
:param powerLevel: 0x00 = 0mA, 0x7F = 25.4mA, 0xFF = 50mA
:param sampleAverage: int, 1,2,4,8,16,32, default is 4
:param ledMode: 1 = RED, 2=RED+IR , 3=RED+IR+GREEN
:param sampleRate: 0-3200
:param pulseWidth: 0-411 (microseconds)
:param adcRange: 2048,4096,8192,16384
:return: no return value
"""
self.softReset() # Reset all configuration, threshold, and data registers to POR values
# FIFO Configuration
# -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
# The chip will average multiple samples of same type together if you wish
if sampleAverage == 1:
self.setFIFOAverage(MAX30105_SAMPLEAVG_1) # No averaging per FIFO record
elif sampleAverage == 2:
self.setFIFOAverage(MAX30105_SAMPLEAVG_2)
elif sampleAverage == 4:
self.setFIFOAverage(MAX30105_SAMPLEAVG_4)
elif sampleAverage == 8:
self.setFIFOAverage(MAX30105_SAMPLEAVG_8)
elif sampleAverage == 16:
self.setFIFOAverage(MAX30105_SAMPLEAVG_16)
elif sampleAverage == 32:
self.setFIFOAverage(MAX30105_SAMPLEAVG_32)
else:
self.setFIFOAverage(MAX30105_SAMPLEAVG_4)
# setFIFOAlmostFull(2) # Set to 30 samples to trigger an 'Almost Full' interrupt
self.enableFIFORollover() # Allow FIFO to wrap/roll over
# -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
# Mode Configuration
# -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
if ledMode == 3:
self.setLEDMode(MAX30105_MODE_MULTILED) # Watch all three LED channels
elif ledMode == 2:
self.setLEDMode(MAX30105_MODE_REDIRONLY) # Red and IR
else:
self.setLEDMode(MAX30105_MODE_REDONLY) # Red only
self.activeLEDs = ledMode # Used to control how many bytes to read from FIFO buffer
# -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
# Particle Sensing Configuration
# -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
if adcRange < 4096:
self.setADCRange(MAX30105_ADCRANGE_2048) # 7.81pA per LSB
elif adcRange < 8192:
self.setADCRange(MAX30105_ADCRANGE_4096) # 15.63pA per LSB
elif adcRange < 16384:
self.setADCRange(MAX30105_ADCRANGE_8192) # 31.25pA per LSB
elif adcRange == 16384:
self.setADCRange(MAX30105_ADCRANGE_16384) # 62.5pA per LSB
else:
self.setADCRange(MAX30105_ADCRANGE_2048)
if sampleRate < 100:
self.setSampleRate(MAX30105_SAMPLERATE_50) # Take 50 samples per second
elif sampleRate < 200:
self.setSampleRate(MAX30105_SAMPLERATE_100)
elif sampleRate < 400:
self.setSampleRate(MAX30105_SAMPLERATE_200)
elif sampleRate < 800:
self.setSampleRate(MAX30105_SAMPLERATE_400)
elif sampleRate < 1000:
self.setSampleRate(MAX30105_SAMPLERATE_800)
elif sampleRate < 1600:
self.setSampleRate(MAX30105_SAMPLERATE_1000)
elif sampleRate < 3200:
self.setSampleRate(MAX30105_SAMPLERATE_1600)
elif sampleRate == 3200:
self.setSampleRate(MAX30105_SAMPLERATE_3200)
else:
self.setSampleRate(MAX30105_SAMPLERATE_50)
# The longer the pulse width the longer range of detection you'll have
# At 69us and 0.4mA it's about 2 inches
# At 411us and 0.4mA it's about 6 inches
if pulseWidth < 118:
self.setPulseWidth(MAX30105_PULSEWIDTH_69) # Page 26, Gets us 15 bit resolution
elif pulseWidth < 215:
self.setPulseWidth(MAX30105_PULSEWIDTH_118) # 16 bit resolution
elif pulseWidth < 411:
self.setPulseWidth(MAX30105_PULSEWIDTH_215) # 17 bit resolution
elif pulseWidth == 411:
self.setPulseWidth(MAX30105_PULSEWIDTH_411) # 18 bit resolution
else:
self.setPulseWidth(MAX30105_PULSEWIDTH_69)
# -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
# LED Pulse Amplitude Configuration
# -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
# Default is 0x1F which gets us 6.4mA
# powerLevel = 0x02, 0.4mA - Presence detection of ~4 inch
# powerLevel = 0x1F, 6.4mA - Presence detection of ~8 inch
# powerLevel = 0x7F, 25.4mA - Presence detection of ~8 inch
# powerLevel = 0xFF, 50.0mA - Presence detection of ~12 inch
self.setPulseAmplitudeRed(powerLevel)
self.setPulseAmplitudeIR(powerLevel)
self.setPulseAmplitudeGreen(powerLevel)
self.setPulseAmplitudeProximity(powerLevel)
# -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
# Multi-LED Mode Configuration, Enable the reading of the three LEDs
# -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
self.enableSlot(1, SLOT_RED_LED)
if ledMode > 1:
self.enableSlot(2, SLOT_IR_LED)
if ledMode > 2:
self.enableSlot(3, SLOT_GREEN_LED)
# self.enableSlot(1, SLOT_RED_PILOT)
# self.nableSlot(2, SLOT_IR_PILOT)
# self.enableSlot(3, SLOT_GREEN_PILOT)
# -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
self.clearFIFO() # Reset the FIFO before we begin checking the sensor
#
# Data Collection
#
#
# available()
#
# Tell caller how many samples are available
[docs] def available(self):
"""
Tell caller how many samples are available
:return: number of samples available
:rtype: integer
"""
numberOfSamples = self.head - self.tail
if numberOfSamples < 0:
numberOfSamples += self.STORAGE_SIZE
return numberOfSamples
#
# nextSample()
#
# Advance the tail
[docs] def nextSample(self):
"""
Advance the tail
:return: no return value
"""
if self.available(): #Only advance the tail if new data is available
self.tail += 1
self.tail %= self.STORAGE_SIZE #Wrap condition
#
# check()
#
# Polls the sensor for new data
# Call regularly
# If new data is available, it updates the head and tail in the main lists of data
# Returns number of new samples obtained
[docs] def check(self):
"""
Polls the sensor for new data
Call regularly
If new data is available, it updates the head and tail in the main lists of data
:return: number of new samples obtained
:rtype: integer
"""
# Read register FIDO_DATA (3-byte * number of active LED)
# Until FIFO_RD_PTR = FIFO_WR_PTR
readPointer = self.getReadPointer()
writePointer = self.getWritePointer()
numberOfSamples = 0
#Do we have new data?
if readPointer != writePointer:
#Calculate the number of samples we need to get from sensor
numberOfSamples = (writePointer - readPointer)
if (numberOfSamples < 0):
numberOfSamples += 32 #Wrap condition
#We now have the number of samples, now calc bytes to read
bytesToRead = numberOfSamples * self.activeLEDs * 3
# Send command to prepare to read the FIFODATA location from sensor
self._i2c.writeCommand(self.address, MAX30105_FIFODATA)
# Block Read (>32 bytes) bytesToRead from the sensor
with SMBus(1) as bus:
msg = i2c_msg.read(self.address, bytesToRead)
bus.i2c_rdwr(msg)
buff = list(msg)
# Grab all the bytes we just read into buff, and plug them into the correct local variables.
# Note, we need to keep track of where we are in the buff (using sampleNumber and buffIndex "i")
# Also, the bytes in buff will mean different things,
# according to which LEDs are active (Red, IR, and/or Green)
# for loop through the entire buff list,
# using sampleNumber to "jump" buffIndex as necessary to each sample start within buff
for sampleNumber in range(0, numberOfSamples):
buffIndex = sampleNumber * self.activeLEDs * 3 # move index to the start of the next sample
self.head += 1 # Advance the head of the storage list
self.head %= self.STORAGE_SIZE # Wrap condition
# First 3 bytes in buff will always be RED
tempByte2 = buff[buffIndex+0]
tempByte1 = buff[buffIndex+1]
tempByte0 = buff[buffIndex+2]
#also copy to varaibles for testing
#self.byte0 = tempByte0
#self.byte1 = tempByte1
#self.byte2 = tempByte2
# Combine bytes into single 32 bit integer variable
tempLong = 0
tempLong |= tempByte0
tempLong |= (tempByte1 << 8)
tempLong |= (tempByte2 << 16)
tempLong &= 0x3FFFF # Zero out all but 18 bits
self.red[self.head] = tempLong # Store this reading into the red list at head
#self.red[self.head] = tempByte0 # Store this reading into the red list at head
if (self.activeLEDs > 1):
# Next 3 bytes in buff will be IR
tempByte2 = buff[buffIndex+3]
tempByte1 = buff[buffIndex+4]
tempByte0 = buff[buffIndex+5]
# Combine bytes into single 32 bit integer variable
tempLong = 0
tempLong |= tempByte0
tempLong |= (tempByte1 << 8)
tempLong |= (tempByte2 << 16)
tempLong &= 0x3FFFF # Zero out all but 18 bits
self.IR[self.head] = tempLong # Store this reading into the IR list at head
if (self.activeLEDs > 2):
# Next 3 bytes in buff will be GREEN
tempByte2 = buff[buffIndex+6]
tempByte1 = buff[buffIndex+7]
tempByte0 = buff[buffIndex+8]
# Combine bytes into single 32 bit integer variable
tempLong = 0
tempLong |= tempByte0
tempLong |= (tempByte1 << 8)
tempLong |= (tempByte2 << 16)
tempLong &= 0x3FFFF # Zero out all but 18 bits
self.green[self.head] = tempLong # Store this reading into the IR list at head
return numberOfSamples #Let the world know how much new data we found
#
# safeCheck()
#
# Check for new data but give up after a certain amount of time
# Returns true if new data was found
# Returns false if new data was not found
[docs] def safeCheck(self, maxTimeToCheck):
"""
Check for new data but give up after a certain amount of time
Returns true if new data was found
Returns false if new data was not found
:param maxTimeToCheck: milliseconds to timeout
:return: True if new data was found, otherwise False
:rtype: boolean
"""
timeout = maxTimeToCheck
while(timeout):
if(self.check() > 0): #We found new data!
return True
time.sleep(0.001)
timeout -= 1
return False
#
# getRed()
#
# Report the most recent red value
[docs] def getRed(self):
"""
Report the most recent red value
:return: value of RED light sensor from most recent sample
:rtype: integer
"""
#Check the sensor for new data for 250ms
if self.safeCheck(250):
return self.red[self.head]
else:
return 0 #Sensor failed to find new data
#
# getIR()
#
# Report the most recent IR value
[docs] def getIR(self):
"""
Report the most recent IR value
:return: value of IR light sensor from most recent sample
:rtype: integer
"""
#Check the sensor for new data for 250ms
if self.safeCheck(250):
return (self.IR[self.head])
else:
return 0 #Sensor failed to find new data
#
# getGreen()
#
# Report the most recent Green value
[docs] def getGreen(self):
"""
Report the most recent GREEN value
:return: value of GREEN light sensor from most recent sample
:rtype: integer
"""
#Check the sensor for new data for 250ms
if self.safeCheck(250):
return self.green[self.head]
else:
return 0 #Sensor failed to find new data
#
# getFIFORed()
#
# Report the next Red value in the FIFO
[docs] def getFIFORed(self):
"""
Report the next Red value in the FIFO
:return: the next Red value in the FIFO
:rtype: integer
"""
return self.red[self.tail]
#
# getFIFOIR()
#
# Report the next IR value in the FIFO
[docs] def getFIFOIR(self):
"""
Report the next IR value in the FIFO
:return: the next IR value in the FIFO
:rtype: integer
"""
return self.IR[self.tail]
#
# getFIFOGreen()
#
# Report the next Green value in the FIFO
[docs] def getFIFOGreen(self):
"""
Report the next Green value in the FIFO
:return: the next Green value in the FIFO
:rtype: integer
"""
return self.green[self.tail]
#
# Device ID and Revision
#
#
# readPartID()
#
# Report Part ID from the sensor
[docs] def readPartID(self):
"""
Report Part ID from the sensor
:return: Part ID
:rtype: integer
"""
return self._i2c.readByte(self.address, MAX30105_PARTID)
#
# readRevisionID()
#
# Report Revision ID from the sensor
[docs] def readRevisionID(self):
"""
Report Revision ID from the sensor
:return: Revision ID
:rtype: integer
"""
self.revisionID = self._i2c.readByte(self.address, MAX30105_REVISIONID)
#
# getRevisionID()
#
# Report Revision ID from current variable in this class
[docs] def getRevisionID(self):
"""
Report Revision ID from current variable in this class
:return: Revision ID
:rtype: integer
"""
return self.revisionID
#
# readTemperature()
#
# Read Die Temperature in C
#
[docs] def readTemperature(self):
"""
Report Die Temperature in C
:return: die temp in C
:rtype: float
"""
# DIE_TEMP_RDY interrupt must be enabled
# See issue 19: https://github.com/sparkfun/SparkFun_MAX3010x_Sensor_Library/issues/19
# Step 1: Config die temperature register to take 1 temperature sample
self._i2c.writeByte(self.address, MAX30105_DIETEMPCONFIG, 0x01)
# Poll for bit to clear, reading is then complete
# Timeout after 100ms
startTime = self.millis()
while ( ( self.millis() - startTime ) < 100 ):
# Check to see if DIE_TEMP_RDY interrupt is set
response = self._i2c.readByte(self.address, MAX30105_INTSTAT2)
if ((response & MAX30105_INT_DIE_TEMP_RDY_ENABLE) > 0):
break # We're done!
time.sleep(0.001) # Let's not over burden the I2C bus
# Step 2: Read die temperature register (integer)
tempInt = self._i2c.readByte(self.address, MAX30105_DIETEMPINT)
tempFrac = self._i2c.readByte(self.address, MAX30105_DIETEMPFRAC) # Causes the clearing of the DIE_TEMP_RDY interrupt
tempInt = float(tempInt)
tempFrac = float(tempFrac)
# Step 3: Calculate temperature (datasheet pg. 23)
return (tempInt + (tempFrac * 0.0625))
#
# readTemperatureF()
#
# Returns die temp in F
#
[docs] def readTemperatureF(self):
"""
Returns die temp in F
:return: die temp in F
:rtype: float
"""
temp = self.readTemperature()
if (temp != -999.0):
temp = ( (temp * 1.8) + 32.0 )
return temp
#
# checkForBeat()
#
# Wrapper function to allow access to function within supporting heart_rate.py file
#
[docs] def checkForBeat(self, sample):
"""
Wrapper function to allow access to function within supporting heart_rate.py file
:param sample: IR sample
:return: True if a beat is detected, otherwise False
:rtype: boolean
"""
return hr.checkForBeat(sample)