From 9979fcfcca2568de1de8ca96e78d21e5b8e96523 Mon Sep 17 00:00:00 2001 From: Nicola <62206011+n-elia@users.noreply.github.com> Date: Wed, 6 Jul 2022 23:00:14 +0200 Subject: [PATCH 01/13] Removes unused import --- max30102/max30102.py | 1 - 1 file changed, 1 deletion(-) diff --git a/max30102/max30102.py b/max30102/max30102.py index 4de7ff7..fd50fe4 100644 --- a/max30102/max30102.py +++ b/max30102/max30102.py @@ -14,7 +14,6 @@ # This driver aims at giving almost full access to Maxim MAX30102 functionalities. # n-elia -import uerrno from machine import SoftI2C from ustruct import unpack from utime import sleep_ms, ticks_diff, ticks_ms From e4e2a3ee345c393f94fc4d119b90610c077ed469 Mon Sep 17 00:00:00 2001 From: Nicola <62206011+n-elia@users.noreply.github.com> Date: Sat, 3 Dec 2022 16:21:53 +0100 Subject: [PATCH 02/13] Adds some details about compatibility with `mip` (WIP) and improves manual installation guide. --- README.md | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c384420..3443e8d 100644 --- a/README.md +++ b/README.md @@ -46,12 +46,14 @@ A full example is provided in `/example` directory. #### 1a - **network-enabled MicroPython ports** +> Warning: in latest MicroPython releases `upip` has been deprecated in favor of [`mip`](https://docs.micropython.org/en/latest/reference/packages.html#package-management). Please use the manual installation until I set up the driver to work with `mip`. + To include the library into a network-enabled MicroPython project, it's sufficient to install the package using `upip`: ```python import upip -upip.install(micropython - max30102) +upip.install("micropython-max30102") ``` Make sure that your firmware runs these lines **after** an Internet connection has been established. @@ -62,7 +64,20 @@ content into your microcontroller. If you prefer, you can perform a manual insta #### 1b - **manual way** (no Internet access required) To directly include the library into a MicroPython project, it's sufficient to copy `max30102/circular_buffer.py` -and `max30102/max30102.py` next to your `main.py` file, into a `lib` directory. Then, import the constructor as follows: +and `max30102/max30102.py` next to your `main.py` file, into a `lib` directory. + +The folder tree should look as follows: + +```text +. +โ”ฃ ๐Ÿ“œ boot.py +โ”ฃ ๐Ÿ“œ main.py +โ”— ๐Ÿ“‚ lib + โ”ฃ ๐Ÿ“œ max30102.py + โ”— ๐Ÿ“œ circular_buffer.py +``` + +Then, import the constructor as follows: ```python from max30102 import MAX30102 From 3d318fe9f123553be14afe6e74efe3c95d59b444 Mon Sep 17 00:00:00 2001 From: Nicola <62206011+n-elia@users.noreply.github.com> Date: Sun, 4 Dec 2022 00:27:18 +0100 Subject: [PATCH 03/13] Support to mip package manager (#16) * Changes module's files structure. The module import does not change with this commit. * Adds package.json for the package to be available via mip and mpremote. * Adds support to mip. * Bumps to v0.4.1. --- README.md | 23 +- example/boot.py | 17 +- max30102/__init__.py | 699 +++++++++++++++++++++++++++++++++++++++++++ max30102/max30102.py | 699 ------------------------------------------- package.json | 9 + setup.py | 2 +- 6 files changed, 737 insertions(+), 712 deletions(-) delete mode 100644 max30102/max30102.py create mode 100644 package.json diff --git a/README.md b/README.md index 3443e8d..e47c784 100644 --- a/README.md +++ b/README.md @@ -46,9 +46,17 @@ A full example is provided in `/example` directory. #### 1a - **network-enabled MicroPython ports** -> Warning: in latest MicroPython releases `upip` has been deprecated in favor of [`mip`](https://docs.micropython.org/en/latest/reference/packages.html#package-management). Please use the manual installation until I set up the driver to work with `mip`. +> Warning: in latest MicroPython releases `upip` has been deprecated in favor of [`mip`](https://docs.micropython.org/en/latest/reference/packages.html#package-management). This module is compatible with both of them. Please use the package manager included into your MicroPython version. -To include the library into a network-enabled MicroPython project, it's sufficient to install the package using `upip`: +If your MicroPython version supports `mip` package manager, put these lines **after** the setup of an Internet connection: + +```python +import mip + +mip.install("github:n-elia/MAX30102-MicroPython-driver") +``` + +If your MicroPython version supports `upip` package manager, put these lines **after** the setup of an Internet connection: ```python import upip @@ -56,15 +64,13 @@ import upip upip.install("micropython-max30102") ``` -Make sure that your firmware runs these lines **after** an Internet connection has been established. - To run the example in `./example` folder, please set your WiFi credentials in `boot.py` and then upload `./example` content into your microcontroller. If you prefer, you can perform a manual install as explained below. #### 1b - **manual way** (no Internet access required) To directly include the library into a MicroPython project, it's sufficient to copy `max30102/circular_buffer.py` -and `max30102/max30102.py` next to your `main.py` file, into a `lib` directory. +and `max30102/__init__.py` next to your `main.py` file, into a `lib` directory. The folder tree should look as follows: @@ -73,7 +79,7 @@ The folder tree should look as follows: โ”ฃ ๐Ÿ“œ boot.py โ”ฃ ๐Ÿ“œ main.py โ”— ๐Ÿ“‚ lib - โ”ฃ ๐Ÿ“œ max30102.py + โ”ฃ ๐Ÿ“œ __init__.py โ”— ๐Ÿ“œ circular_buffer.py ``` @@ -83,7 +89,7 @@ Then, import the constructor as follows: from max30102 import MAX30102 ``` -To run the example in `./example` folder, copy `max30102/circular_buffer.py` and `max30102/max30102.py` into +To run the example in `./example` folder, copy `max30102/circular_buffer.py` and `max30102/__init__.py` into the `./example/lib` directory. Then, upload the `./example` directory content into your microcontroller. After the upload, press the reset button of your board are you're good to go. @@ -273,6 +279,9 @@ resolution of 0.0625ยฐC, but be aware that the accuracy is ยฑ1ยฐC. ## Changelog +- v0.4.1 + - Changed the module files organization. + - Added support to `mip` package manager. - v0.4.0 - According to some best practices discussed [here](https://forum.micropython.org/viewtopic.php?f=2&t=12508), some changes have been made. diff --git a/example/boot.py b/example/boot.py index 394077e..9b722cf 100644 --- a/example/boot.py +++ b/example/boot.py @@ -17,13 +17,20 @@ def do_connect(ssid: str, password: str): my_ssid = "my_ssid" my_pass = "my_password" + # Check if the module is available in memory try: from max30102 import MAX30102 - except: - print("'max30102' not found!") + except ImportError as e: + # Module not available. Try to connect to Internet to download it. + print(f"Import error: {e}") + print("Trying to connect to the Internet to download the module.") + do_connect(my_ssid, my_pass) try: + # Try to leverage upip package manager to download the module. import upip - do_connect(my_ssid, my_pass) upip.install("micropython-max30102") - except: - print("Unable to get 'micropython-max30102' package!") + except ImportError: + # upip not available. Try to leverage mip package manager to download the module. + print("upip not available in this port. Trying with mip.") + import mip + mip.install("github:n-elia/MAX30102-MicroPython-driver") diff --git a/max30102/__init__.py b/max30102/__init__.py index e69de29..79fe6fb 100644 --- a/max30102/__init__.py +++ b/max30102/__init__.py @@ -0,0 +1,699 @@ +# This work is a lot based on: +# - https://github.com/sparkfun/SparkFun_MAX3010x_Sensor_Library +# Written by Peter Jansen and Nathan Seidle (SparkFun) +# This is a library written for the Maxim MAX30105 Optical Smoke Detector +# It should also work with the MAX30105, which has a Green LED, too. +# These sensors use I2C to communicate, as well as a single (optional) +# interrupt line that is not currently supported in this driver. +# Written by Peter Jansen and Nathan Seidle (SparkFun) +# BSD license, all text above must be included in any redistribution. +# +# - https://github.com/kandizzy/esp32-micropython/blob/master/PPG/ppg/MAX30105.py +# A port of the library to MicroPython by kandizzy +# +# This driver aims at giving almost full access to Maxim MAX30102 functionalities. +# n-elia + +from machine import SoftI2C +from ustruct import unpack +from utime import sleep_ms, ticks_diff, ticks_ms + +from max30102.circular_buffer import CircularBuffer + +# I2C address (7-bit address) +MAX3010X_I2C_ADDRESS = 0x57 # Right-shift of 0xAE, 0xAF + +# Status Registers +MAX30105_INT_STAT_1 = 0x00 +MAX30105_INT_STAT_2 = 0x01 +MAX30105_INT_ENABLE_1 = 0x02 +MAX30105_INT_ENABLE_2 = 0x03 + +# FIFO Registers +MAX30105_FIFO_WRITE_PTR = 0x04 +MAX30105_FIFO_OVERFLOW = 0x05 +MAX30105_FIFO_READ_PTR = 0x06 +MAX30105_FIFO_DATA = 0x07 + +# Configuration Registers +MAX30105_FIFO_CONFIG = 0x08 +MAX30105_MODE_CONFIG = 0x09 +MAX30105_PARTICLE_CONFIG = 0x0A # Sometimes listed as 'SPO2' in datasheet (pag.11) +MAX30105_LED1_PULSE_AMP = 0x0C # IR +MAX30105_LED2_PULSE_AMP = 0x0D # RED +MAX30105_LED3_PULSE_AMP = 0x0E # GREEN (when available) +MAX30105_LED_PROX_AMP = 0x10 +MAX30105_MULTI_LED_CONFIG_1 = 0x11 +MAX30105_MULTI_LED_CONFIG_2 = 0x12 + +# Die Temperature Registers +MAX30105_DIE_TEMP_INT = 0x1F +MAX30105_DIE_TEMP_FRAC = 0x20 +MAX30105_DIE_TEMP_CONFIG = 0x21 + +# Proximity Function Registers +MAX30105_PROX_INT_THRESH = 0x30 + +# Part ID Registers +MAX30105_REVISION_ID = 0xFE +MAX30105_PART_ID = 0xFF # Should always be 0x15. Identical for MAX30102. + +# MAX30105 Commands +# Interrupt configuration (datasheet pag 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 + +# FIFO data queue configuration +MAX30105_SAMPLE_AVG_MASK = ~0b11100000 +MAX30105_SAMPLE_AVG_1 = 0x00 +MAX30105_SAMPLE_AVG_2 = 0x20 +MAX30105_SAMPLE_AVG_4 = 0x40 +MAX30105_SAMPLE_AVG_8 = 0x60 +MAX30105_SAMPLE_AVG_16 = 0x80 +MAX30105_SAMPLE_AVG_32 = 0xA0 + +MAX30105_ROLLOVER_MASK = 0xEF +MAX30105_ROLLOVER_ENABLE = 0x10 +MAX30105_ROLLOVER_DISABLE = 0x00 +# Mask for 'almost full' interrupt (defaults to 32 samples) +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_RED_ONLY = 0x02 +MAX30105_MODE_RED_IR_ONLY = 0x03 +MAX30105_MODE_MULTI_LED = 0x07 + +# Particle sensing configuration commands (pgs 19-20) +MAX30105_ADC_RANGE_MASK = 0x9F +MAX30105_ADC_RANGE_2048 = 0x00 +MAX30105_ADC_RANGE_4096 = 0x20 +MAX30105_ADC_RANGE_8192 = 0x40 +MAX30105_ADC_RANGE_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_PULSE_WIDTH_MASK = 0xFC +MAX30105_PULSE_WIDTH_69 = 0x00 +MAX30105_PULSE_WIDTH_118 = 0x01 +MAX30105_PULSE_WIDTH_215 = 0x02 +MAX30105_PULSE_WIDTH_411 = 0x03 + +# LED brightness level. It affects the distance of detection. +MAX30105_PULSE_AMP_LOWEST = 0x02 # 0.4mA - Presence detection of ~4 inch +MAX30105_PULSE_AMP_LOW = 0x1F # 6.4mA - Presence detection of ~8 inch +MAX30105_PULSE_AMP_MEDIUM = 0x7F # 25.4mA - Presence detection of ~8 inch +MAX30105_PULSE_AMP_HIGH = 0xFF # 50.0mA - Presence detection of ~12 inch + +# Multi-LED Mode configuration (datasheet pag 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_EXPECTED_PART_ID = 0x15 + +# Size of the queued readings +STORAGE_QUEUE_SIZE = 4 + + +# Data structure to hold the last readings +class SensorData: + def __init__(self): + self.red = CircularBuffer(STORAGE_QUEUE_SIZE) + self.IR = CircularBuffer(STORAGE_QUEUE_SIZE) + self.green = CircularBuffer(STORAGE_QUEUE_SIZE) + + +# Sensor class +class MAX30102(object): + def __init__(self, + i2c: SoftI2C, + i2c_hex_address=MAX3010X_I2C_ADDRESS, + ): + self.i2c_address = i2c_hex_address + self._i2c = i2c + self._active_leds = None + self._pulse_width = None + self._multi_led_read_mode = None + # Store current config values to compute acquisition frequency + self._sample_rate = None + self._sample_avg = None + self._acq_frequency = None + self._acq_frequency_inv = None + # Circular buffer of readings from the sensor + self.sense = SensorData() + + # Sensor setup method + def setup_sensor(self, led_mode=2, adc_range=16384, sample_rate=400, + led_power=MAX30105_PULSE_AMP_MEDIUM, sample_avg=8, + pulse_width=411): + # Reset the sensor's registers from previous configurations + self.soft_reset() + + # Set the number of samples to be averaged by the chip to 8 + self.set_fifo_average(sample_avg) + + # Allow FIFO queues to wrap/roll over + self.enable_fifo_rollover() + + # Set the LED mode to the default value of 2 (RED + IR) + # Note: the 3rd mode is available only with MAX30105 + self.set_led_mode(led_mode) + + # Set the ADC range to default value of 16384 + self.set_adc_range(adc_range) + + # Set the sample rate to the default value of 400 + self.set_sample_rate(sample_rate) + + # Set the Pulse Width to the default value of 411 + self.set_pulse_width(pulse_width) + + # Set the LED brightness to the default value of 'low' + self.set_pulse_amplitude_red(led_power) + self.set_pulse_amplitude_it(led_power) + self.set_pulse_amplitude_green(led_power) + self.set_pulse_amplitude_proximity(led_power) + + # Clears the FIFO + self.clear_fifo() + + def __del__(self): + self.shutdown() + + # Methods to read the two interrupt flags + def get_int_1(self): + # Load the Interrupt 1 status (configurable) from the register + rev_id = self.i2c_read_register(MAX30105_INT_STAT_1) + return rev_id + + def get_int_2(self): + # Load the Interrupt 2 status (DIE_TEMP_DRY) from the register + rev_id = self.i2c_read_register(MAX30105_INT_STAT_2) + return rev_id + + # Methods to set up the interrupt flags + def enable_a_full(self): + # Enable the almost full interrupt (datasheet pag. 13) + self.bitmask(MAX30105_INT_ENABLE_1, MAX30105_INT_A_FULL_MASK, MAX30105_INT_A_FULL_ENABLE) + + def disable_a_full(self): + # Disable the almost full interrupt (datasheet pag. 13) + self.bitmask(MAX30105_INT_ENABLE_1, MAX30105_INT_A_FULL_MASK, MAX30105_INT_A_FULL_DISABLE) + + def enable_data_rdy(self): + # Enable the new FIFO data ready interrupt (datasheet pag. 13) + self.bitmask(MAX30105_INT_ENABLE_1, MAX30105_INT_DATA_RDY_MASK, MAX30105_INT_DATA_RDY_ENABLE) + + def disable_data_rdy(self): + # Disable the new FIFO data ready interrupt (datasheet pag. 13) + self.bitmask(MAX30105_INT_ENABLE_1, MAX30105_INT_DATA_RDY_MASK, MAX30105_INT_DATA_RDY_DISABLE) + + def enable_alc_ovf(self): + # Enable the ambient light limit interrupt (datasheet pag. 13) + self.bitmask(MAX30105_INT_ENABLE_1, MAX30105_INT_ALC_OVF_MASK, MAX30105_INT_ALC_OVF_ENABLE) + + def disable_alc_ovf(self): + # Disable the ambient light limit interrupt (datasheet pag. 13) + self.bitmask(MAX30105_INT_ENABLE_1, MAX30105_INT_ALC_OVF_MASK, MAX30105_INT_ALC_OVF_DISABLE) + + def enable_prox_int(self): + # Enable the proximity interrupt (datasheet pag. 13) + self.bitmask(MAX30105_INT_ENABLE_1, MAX30105_INT_PROX_INT_MASK, MAX30105_INT_PROX_INT_ENABLE) + + def disable_prox_int(self): + # Disable the proximity interrupt (datasheet pag. 13) + self.bitmask(MAX30105_INT_ENABLE_1, MAX30105_INT_PROX_INT_MASK, MAX30105_INT_PROX_INT_DISABLE) + + def enable_die_temp_rdy(self): + # Enable the die temp. conversion finish interrupt (datasheet pag. 13) + self.bitmask(MAX30105_INT_ENABLE_2, MAX30105_INT_DIE_TEMP_RDY_MASK, MAX30105_INT_DIE_TEMP_RDY_ENABLE) + + def disable_die_temp_rdy(self): + # Disable the die temp. conversion finish interrupt (datasheet pag. 13) + self.bitmask(MAX30105_INT_ENABLE_2, MAX30105_INT_DIE_TEMP_RDY_MASK, MAX30105_INT_DIE_TEMP_RDY_DISABLE) + + # Configuration reset + + def soft_reset(self): + # When the RESET bit is set to one, all configuration, threshold, + # and data registers are reset to their power-on-state through + # a power-on reset. The RESET bit is cleared automatically back to zero + # after the reset sequence is completed. (datasheet pag. 19) + self.set_bitmask(MAX30105_MODE_CONFIG, MAX30105_RESET_MASK, MAX30105_RESET) + curr_status = -1 + while not ((curr_status & MAX30105_RESET) == 0): + sleep_ms(10) + curr_status = ord(self.i2c_read_register(MAX30105_MODE_CONFIG)) + + # Power states methods + def shutdown(self): + # 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). + self.set_bitmask(MAX30105_MODE_CONFIG, MAX30105_SHUTDOWN_MASK, MAX30105_SHUTDOWN) + + def wakeup(self): + # Pull IC out of low power mode (datasheet pg. 19) + self.set_bitmask(MAX30105_MODE_CONFIG, MAX30105_SHUTDOWN_MASK, MAX30105_WAKEUP) + + # LED Configuration + + def set_led_mode(self, LED_mode): + # Set LED mode: select which LEDs are used for sampling + # Options: RED only, RED + IR only, or ALL (datasheet pag. 19) + if LED_mode == 1: + self.set_bitmask(MAX30105_MODE_CONFIG, MAX30105_MODE_MASK, MAX30105_MODE_RED_ONLY) + elif LED_mode == 2: + self.set_bitmask(MAX30105_MODE_CONFIG, MAX30105_MODE_MASK, MAX30105_MODE_RED_IR_ONLY) + elif LED_mode == 3: + self.set_bitmask(MAX30105_MODE_CONFIG, MAX30105_MODE_MASK, MAX30105_MODE_MULTI_LED) + else: + raise ValueError('Wrong LED mode:{0}!'.format(LED_mode)) + + # Multi-LED Mode Configuration: enable the reading of the LEDs + # depending on the chosen mode + self.enable_slot(1, SLOT_RED_LED) + if LED_mode > 1: + self.enable_slot(2, SLOT_IR_LED) + if LED_mode > 2: + self.enable_slot(3, SLOT_GREEN_LED) + + # Store the LED mode used to control how many bytes to read from + # FIFO buffer in multiLED mode: a sample is made of 3 bytes + self._active_leds = LED_mode + self._multi_led_read_mode = LED_mode * 3 + + # ADC Configuration + def set_adc_range(self, ADC_range): + # ADC range: set the range of the conversion + # Options: 2048, 4096, 8192, 16384 + # Current draw: 7.81pA. 15.63pA, 31.25pA, 62.5pA per LSB. + if ADC_range == 2048: + r = MAX30105_ADC_RANGE_2048 + elif ADC_range == 4096: + r = MAX30105_ADC_RANGE_4096 + elif ADC_range == 8192: + r = MAX30105_ADC_RANGE_8192 + elif ADC_range == 16384: + r = MAX30105_ADC_RANGE_16384 + else: + raise ValueError('Wrong ADC range:{0}!'.format(ADC_range)) + + self.set_bitmask(MAX30105_PARTICLE_CONFIG, MAX30105_ADC_RANGE_MASK, r) + + # Sample Rate Configuration + def set_sample_rate(self, sample_rate): + # Sample rate: select the number of samples taken per second. + # Options: 50, 100, 200, 400, 800, 1000, 1600, 3200 + # Note: in theory, the resulting acquisition frequency for the end user + # is sampleRate/sampleAverage. However, it is worth testing it before + # assuming that the sensor can effectively sustain that frequency + # given its configuration. + if sample_rate == 50: + sr = MAX30105_SAMPLERATE_50 + elif sample_rate == 100: + sr = MAX30105_SAMPLERATE_100 + elif sample_rate == 200: + sr = MAX30105_SAMPLERATE_200 + elif sample_rate == 400: + sr = MAX30105_SAMPLERATE_400 + elif sample_rate == 800: + sr = MAX30105_SAMPLERATE_800 + elif sample_rate == 1000: + sr = MAX30105_SAMPLERATE_1000 + elif sample_rate == 1600: + sr = MAX30105_SAMPLERATE_1600 + elif sample_rate == 3200: + sr = MAX30105_SAMPLERATE_3200 + else: + raise ValueError('Wrong sample rate:{0}!'.format(sample_rate)) + + self.set_bitmask(MAX30105_PARTICLE_CONFIG, MAX30105_SAMPLERATE_MASK, sr) + + # Store the sample rate and recompute the acq. freq. + self._sample_rate = sample_rate + self.update_acquisition_frequency() + + # Pulse width Configuration + def set_pulse_width(self, pulse_width): + # Pulse width of LEDs: The longer the pulse width the longer range of + # detection. At 69us and 0.4mA it's about 2 inches, + # at 411us and 0.4mA it's about 6 inches. + if pulse_width == 69: + pw = MAX30105_PULSE_WIDTH_69 + elif pulse_width == 118: + pw = MAX30105_PULSE_WIDTH_118 + elif pulse_width == 215: + pw = MAX30105_PULSE_WIDTH_215 + elif pulse_width == 411: + pw = MAX30105_PULSE_WIDTH_411 + else: + raise ValueError('Wrong pulse width:{0}!'.format(pulse_width)) + self.set_bitmask(MAX30105_PARTICLE_CONFIG, MAX30105_PULSE_WIDTH_MASK, pw) + + # Store the pulse width + self._pulse_width = pw + + # LED Pulse Amplitude Configuration methods + def set_active_leds_amplitude(self, amplitude): + if self._active_leds > 0: + self.set_pulse_amplitude_red(amplitude) + if self._active_leds > 1: + self.set_pulse_amplitude_it(amplitude) + if self._active_leds > 2: + self.set_pulse_amplitude_green(amplitude) + + def set_pulse_amplitude_red(self, amplitude): + self.i2c_set_register(MAX30105_LED1_PULSE_AMP, amplitude) + + def set_pulse_amplitude_it(self, amplitude): + self.i2c_set_register(MAX30105_LED2_PULSE_AMP, amplitude) + + def set_pulse_amplitude_green(self, amplitude): + self.i2c_set_register(MAX30105_LED3_PULSE_AMP, amplitude) + + def set_pulse_amplitude_proximity(self, amplitude): + self.i2c_set_register(MAX30105_LED_PROX_AMP, amplitude) + + def set_proximity_threshold(self, thresh_msb): + # 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. (datasheet page 24) + self.i2c_set_register(MAX30105_PROX_INT_THRESH, thresh_msb) + + # FIFO averaged samples number Configuration + def set_fifo_average(self, number_of_samples): + # FIFO sample avg: set the number of samples to be averaged by the chip. + # Options: MAX30105_SAMPLE_AVG_1, 2, 4, 8, 16, 32 + if number_of_samples == 1: + ns = MAX30105_SAMPLE_AVG_1 + elif number_of_samples == 2: + ns = MAX30105_SAMPLE_AVG_2 + elif number_of_samples == 4: + ns = MAX30105_SAMPLE_AVG_4 + elif number_of_samples == 8: + ns = MAX30105_SAMPLE_AVG_8 + elif number_of_samples == 16: + ns = MAX30105_SAMPLE_AVG_16 + elif number_of_samples == 32: + ns = MAX30105_SAMPLE_AVG_32 + else: + raise ValueError( + 'Wrong number of samples:{0}!'.format(number_of_samples)) + self.set_bitmask(MAX30105_FIFO_CONFIG, MAX30105_SAMPLE_AVG_MASK, ns) + + # Store the number of averaged samples and recompute the acq. freq. + self._sample_avg = number_of_samples + self.update_acquisition_frequency() + + def update_acquisition_frequency(self): + if None in [self._sample_rate, self._sample_avg]: + return + else: + self._acq_frequency = self._sample_rate / self._sample_avg + from math import ceil + + # Compute the time interval to wait before taking a good measure + # (see note in setSampleRate() method) + self._acq_frequency_inv = int(ceil(1000 / self._acq_frequency)) + + def get_acquisition_frequency(self): + return self._acq_frequency + + def clear_fifo(self): + # Resets all points to start in a known state + # Datasheet page 15 recommends clearing FIFO before beginning a read + self.i2c_set_register(MAX30105_FIFO_WRITE_PTR, 0) + self.i2c_set_register(MAX30105_FIFO_OVERFLOW, 0) + self.i2c_set_register(MAX30105_FIFO_READ_PTR, 0) + + def enable_fifo_rollover(self): + # FIFO rollover: enable to allow FIFO tro wrap/roll over + self.set_bitmask(MAX30105_FIFO_CONFIG, MAX30105_ROLLOVER_MASK, MAX30105_ROLLOVER_ENABLE) + + def disable_fifo_rollover(self): + # FIFO rollover: disable to disallow FIFO tro wrap/roll over + self.set_bitmask(MAX30105_FIFO_CONFIG, MAX30105_ROLLOVER_MASK, MAX30105_ROLLOVER_DISABLE) + + def set_fifo_almost_full(self, number_of_samples): + # 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 + self.set_bitmask(MAX30105_FIFO_CONFIG, MAX30105_A_FULL_MASK, number_of_samples) + + def get_write_pointer(self): + # Read the FIFO Write Pointer from the register + wp = self.i2c_read_register(MAX30105_FIFO_WRITE_PTR) + return wp + + def get_read_pointer(self): + # Read the FIFO Read Pointer from the register + wp = self.i2c_read_register(MAX30105_FIFO_READ_PTR) + return wp + + # Die Temperature method: returns the temperature in C + def read_temperature(self): + # DIE_TEMP_RDY interrupt must be enabled + # Config die temperature register to take 1 temperature sample + self.i2c_set_register(MAX30105_DIE_TEMP_CONFIG, 0x01) + + # Poll for bit to clear, reading is then complete + reading = ord(self.i2c_read_register(MAX30105_INT_STAT_2)) + sleep_ms(100) + while (reading & MAX30105_INT_DIE_TEMP_RDY_ENABLE) > 0: + reading = ord(self.i2c_read_register(MAX30105_INT_STAT_2)) + sleep_ms(1) + + # Read die temperature register (integer) + tempInt = ord(self.i2c_read_register(MAX30105_DIE_TEMP_INT)) + # Causes the clearing of the DIE_TEMP_RDY interrupt + tempFrac = ord(self.i2c_read_register(MAX30105_DIE_TEMP_FRAC)) + + # Calculate temperature (datasheet pg. 23) + return float(tempInt) + (float(tempFrac) * 0.0625) + + def set_prox_int_tresh(self, val): + # Set the PROX_INT_THRESH (see proximity function on datasheet, pag 10) + self.i2c_set_register(MAX30105_PROX_INT_THRESH, val) + + # DeviceID and Revision methods + def read_part_id(self): + # Load the Device ID from the register + part_id = self.i2c_read_register(MAX30105_PART_ID) + return part_id + + def check_part_id(self): + # Checks the correctness of the Device ID + part_id = ord(self.read_part_id()) + return part_id == MAX_30105_EXPECTED_PART_ID + + def get_revision_id(self): + # Load the Revision ID from the register + rev_id = self.i2c_read_register(MAX30105_REVISION_ID) + return ord(rev_id) + + # Time slots management for multi-LED operation mode + def enable_slot(self, slot_number, device): + # In multi-LED mode, each sample is split into up to four time slots, + # SLOT1 through SLOT4. These control registers determine which LED is + # active in each time slot. (datasheet pag 22) + # Devices are SLOT_RED_LED or SLOT_RED_PILOT (proximity) + # Assigning a SLOT_RED_LED will pulse LED + # Assigning a SLOT_RED_PILOT will detect the proximity + if slot_number == 1: + self.bitmask(MAX30105_MULTI_LED_CONFIG_1, MAX30105_SLOT1_MASK, device) + elif slot_number == 2: + self.bitmask(MAX30105_MULTI_LED_CONFIG_1, MAX30105_SLOT2_MASK, device << 4) + elif slot_number == 3: + self.bitmask(MAX30105_MULTI_LED_CONFIG_2, MAX30105_SLOT3_MASK, device) + elif slot_number == 4: + self.bitmask(MAX30105_MULTI_LED_CONFIG_2, MAX30105_SLOT4_MASK, device << 4) + else: + raise ValueError('Wrong slot number:{0}!'.format(slot_number)) + + def disable_slots(self): + # Clear all the slots assignments + self.i2c_set_register(MAX30105_MULTI_LED_CONFIG_1, 0) + self.i2c_set_register(MAX30105_MULTI_LED_CONFIG_2, 0) + + # Low-level I2C Communication + def i2c_read_register(self, REGISTER, n_bytes=1): + self._i2c.writeto(self.i2c_address, bytearray([REGISTER])) + return self._i2c.readfrom(self.i2c_address, n_bytes) + + def i2c_set_register(self, REGISTER, VALUE): + self._i2c.writeto(self.i2c_address, bytearray([REGISTER, VALUE])) + return + + # Given a register, read it, mask it, and then set the thing + def set_bitmask(self, REGISTER, MASK, NEW_VALUES): + newCONTENTS = (ord(self.i2c_read_register(REGISTER)) & MASK) | NEW_VALUES + self.i2c_set_register(REGISTER, newCONTENTS) + return + + # Given a register, read it and mask it + def bitmask(self, reg, slotMask, thing): + originalContents = ord(self.i2c_read_register(reg)) + originalContents = originalContents & slotMask + self.i2c_set_register(reg, originalContents | thing) + + def fifo_bytes_to_int(self, fifo_bytes): + value = unpack(">i", b'\x00' + fifo_bytes) + return (value[0] & 0x3FFFF) >> self._pulse_width + + # Returns how many samples are available + def available(self): + number_of_samples = len(self.sense.red) + return number_of_samples + + # Get a new red value + def get_red(self): + # Check the sensor for new data for 250ms + if self.safe_check(250): + return self.sense.red.pop_head() + else: + # Sensor failed to find new data + return 0 + + # Get a new IR value + def get_ir(self): + # Check the sensor for new data for 250ms + if self.safe_check(250): + return self.sense.IR.pop_head() + else: + # Sensor failed to find new data + return 0 + + # Get a new green value + def get_green(self): + # Check the sensor for new data for 250ms + if self.safe_check(250): + return self.sense.green.pop_head() + else: + # Sensor failed to find new data + return 0 + + # Note: the following 3 functions are the equivalent of using 'getFIFO' + # methods of the SparkFun library + # Pops the next red value in storage (if available) + def pop_red_from_storage(self): + if len(self.sense.red) == 0: + return 0 + else: + return self.sense.red.pop() + + # Pops the next IR value in storage (if available) + def pop_ir_from_storage(self): + if len(self.sense.IR) == 0: + return 0 + else: + return self.sense.IR.pop() + + # Pops the next green value in storage (if available) + def pop_green_from_storage(self): + if len(self.sense.green) == 0: + return 0 + else: + return self.sense.green.pop() + + # (useless - for comparison purposes only) + def next_sample(self): + if self.available(): + # With respect to the SparkFun library, using a deque object + # allows us to avoid manually advancing of the tail + return True + + # Polls the sensor for new data + def check(self): + # Call continuously to poll the sensor for new data. + read_pointer = ord(self.get_read_pointer()) + write_pointer = ord(self.get_write_pointer()) + + # Do we have new data? + if read_pointer != write_pointer: + # Calculate the number of readings we need to get from sensor + number_of_samples = write_pointer - read_pointer + + # Wrap condition (return to the beginning of 32 samples) + if number_of_samples < 0: + number_of_samples += 32 + + for i in range(number_of_samples): + # Read a number of bytes equal to activeLEDs*3 (= 1 sample) + fifo_bytes = self.i2c_read_register(MAX30105_FIFO_DATA, + self._multi_led_read_mode) + + # Convert the readings from bytes to integers, depending + # on the number of active LEDs + if self._active_leds > 0: + self.sense.red.append( + self.fifo_bytes_to_int(fifo_bytes[0:3]) + ) + + if self._active_leds > 1: + self.sense.IR.append( + self.fifo_bytes_to_int(fifo_bytes[3:6]) + ) + + if self._active_leds > 2: + self.sense.green.append( + self.fifo_bytes_to_int(fifo_bytes[6:9]) + ) + + return True + + else: + return False + + # Check for new data but give up after a certain amount of time + def safe_check(self, max_time_to_check): + mark_time = ticks_ms() + while True: + if ticks_diff(ticks_ms(), mark_time) > max_time_to_check: + # Timeout reached + return False + if self.check(): + # new data found + return True + sleep_ms(1) diff --git a/max30102/max30102.py b/max30102/max30102.py deleted file mode 100644 index fd50fe4..0000000 --- a/max30102/max30102.py +++ /dev/null @@ -1,699 +0,0 @@ -# This work is a lot based on: -# - https://github.com/sparkfun/SparkFun_MAX3010x_Sensor_Library -# Written by Peter Jansen and Nathan Seidle (SparkFun) -# This is a library written for the Maxim MAX30105 Optical Smoke Detector -# It should also work with the MAX30105, which has a Green LED, too. -# These sensors use I2C to communicate, as well as a single (optional) -# interrupt line that is not currently supported in this driver. -# Written by Peter Jansen and Nathan Seidle (SparkFun) -# BSD license, all text above must be included in any redistribution. -# -# - https://github.com/kandizzy/esp32-micropython/blob/master/PPG/ppg/MAX30105.py -# A port of the library to MicroPython by kandizzy -# -# This driver aims at giving almost full access to Maxim MAX30102 functionalities. -# n-elia - -from machine import SoftI2C -from ustruct import unpack -from utime import sleep_ms, ticks_diff, ticks_ms - -from circular_buffer import CircularBuffer - -# I2C address (7-bit address) -MAX3010X_I2C_ADDRESS = 0x57 # Right-shift of 0xAE, 0xAF - -# Status Registers -MAX30105_INT_STAT_1 = 0x00 -MAX30105_INT_STAT_2 = 0x01 -MAX30105_INT_ENABLE_1 = 0x02 -MAX30105_INT_ENABLE_2 = 0x03 - -# FIFO Registers -MAX30105_FIFO_WRITE_PTR = 0x04 -MAX30105_FIFO_OVERFLOW = 0x05 -MAX30105_FIFO_READ_PTR = 0x06 -MAX30105_FIFO_DATA = 0x07 - -# Configuration Registers -MAX30105_FIFO_CONFIG = 0x08 -MAX30105_MODE_CONFIG = 0x09 -MAX30105_PARTICLE_CONFIG = 0x0A # Sometimes listed as 'SPO2' in datasheet (pag.11) -MAX30105_LED1_PULSE_AMP = 0x0C # IR -MAX30105_LED2_PULSE_AMP = 0x0D # RED -MAX30105_LED3_PULSE_AMP = 0x0E # GREEN (when available) -MAX30105_LED_PROX_AMP = 0x10 -MAX30105_MULTI_LED_CONFIG_1 = 0x11 -MAX30105_MULTI_LED_CONFIG_2 = 0x12 - -# Die Temperature Registers -MAX30105_DIE_TEMP_INT = 0x1F -MAX30105_DIE_TEMP_FRAC = 0x20 -MAX30105_DIE_TEMP_CONFIG = 0x21 - -# Proximity Function Registers -MAX30105_PROX_INT_THRESH = 0x30 - -# Part ID Registers -MAX30105_REVISION_ID = 0xFE -MAX30105_PART_ID = 0xFF # Should always be 0x15. Identical for MAX30102. - -# MAX30105 Commands -# Interrupt configuration (datasheet pag 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 - -# FIFO data queue configuration -MAX30105_SAMPLE_AVG_MASK = ~0b11100000 -MAX30105_SAMPLE_AVG_1 = 0x00 -MAX30105_SAMPLE_AVG_2 = 0x20 -MAX30105_SAMPLE_AVG_4 = 0x40 -MAX30105_SAMPLE_AVG_8 = 0x60 -MAX30105_SAMPLE_AVG_16 = 0x80 -MAX30105_SAMPLE_AVG_32 = 0xA0 - -MAX30105_ROLLOVER_MASK = 0xEF -MAX30105_ROLLOVER_ENABLE = 0x10 -MAX30105_ROLLOVER_DISABLE = 0x00 -# Mask for 'almost full' interrupt (defaults to 32 samples) -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_RED_ONLY = 0x02 -MAX30105_MODE_RED_IR_ONLY = 0x03 -MAX30105_MODE_MULTI_LED = 0x07 - -# Particle sensing configuration commands (pgs 19-20) -MAX30105_ADC_RANGE_MASK = 0x9F -MAX30105_ADC_RANGE_2048 = 0x00 -MAX30105_ADC_RANGE_4096 = 0x20 -MAX30105_ADC_RANGE_8192 = 0x40 -MAX30105_ADC_RANGE_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_PULSE_WIDTH_MASK = 0xFC -MAX30105_PULSE_WIDTH_69 = 0x00 -MAX30105_PULSE_WIDTH_118 = 0x01 -MAX30105_PULSE_WIDTH_215 = 0x02 -MAX30105_PULSE_WIDTH_411 = 0x03 - -# LED brightness level. It affects the distance of detection. -MAX30105_PULSE_AMP_LOWEST = 0x02 # 0.4mA - Presence detection of ~4 inch -MAX30105_PULSE_AMP_LOW = 0x1F # 6.4mA - Presence detection of ~8 inch -MAX30105_PULSE_AMP_MEDIUM = 0x7F # 25.4mA - Presence detection of ~8 inch -MAX30105_PULSE_AMP_HIGH = 0xFF # 50.0mA - Presence detection of ~12 inch - -# Multi-LED Mode configuration (datasheet pag 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_EXPECTED_PART_ID = 0x15 - -# Size of the queued readings -STORAGE_QUEUE_SIZE = 4 - - -# Data structure to hold the last readings -class SensorData: - def __init__(self): - self.red = CircularBuffer(STORAGE_QUEUE_SIZE) - self.IR = CircularBuffer(STORAGE_QUEUE_SIZE) - self.green = CircularBuffer(STORAGE_QUEUE_SIZE) - - -# Sensor class -class MAX30102(object): - def __init__(self, - i2c: SoftI2C, - i2c_hex_address=MAX3010X_I2C_ADDRESS, - ): - self.i2c_address = i2c_hex_address - self._i2c = i2c - self._active_leds = None - self._pulse_width = None - self._multi_led_read_mode = None - # Store current config values to compute acquisition frequency - self._sample_rate = None - self._sample_avg = None - self._acq_frequency = None - self._acq_frequency_inv = None - # Circular buffer of readings from the sensor - self.sense = SensorData() - - # Sensor setup method - def setup_sensor(self, led_mode=2, adc_range=16384, sample_rate=400, - led_power=MAX30105_PULSE_AMP_MEDIUM, sample_avg=8, - pulse_width=411): - # Reset the sensor's registers from previous configurations - self.soft_reset() - - # Set the number of samples to be averaged by the chip to 8 - self.set_fifo_average(sample_avg) - - # Allow FIFO queues to wrap/roll over - self.enable_fifo_rollover() - - # Set the LED mode to the default value of 2 (RED + IR) - # Note: the 3rd mode is available only with MAX30105 - self.set_led_mode(led_mode) - - # Set the ADC range to default value of 16384 - self.set_adc_range(adc_range) - - # Set the sample rate to the default value of 400 - self.set_sample_rate(sample_rate) - - # Set the Pulse Width to the default value of 411 - self.set_pulse_width(pulse_width) - - # Set the LED brightness to the default value of 'low' - self.set_pulse_amplitude_red(led_power) - self.set_pulse_amplitude_it(led_power) - self.set_pulse_amplitude_green(led_power) - self.set_pulse_amplitude_proximity(led_power) - - # Clears the FIFO - self.clear_fifo() - - def __del__(self): - self.shutdown() - - # Methods to read the two interrupt flags - def get_int_1(self): - # Load the Interrupt 1 status (configurable) from the register - rev_id = self.i2c_read_register(MAX30105_INT_STAT_1) - return rev_id - - def get_int_2(self): - # Load the Interrupt 2 status (DIE_TEMP_DRY) from the register - rev_id = self.i2c_read_register(MAX30105_INT_STAT_2) - return rev_id - - # Methods to set up the interrupt flags - def enable_a_full(self): - # Enable the almost full interrupt (datasheet pag. 13) - self.bitmask(MAX30105_INT_ENABLE_1, MAX30105_INT_A_FULL_MASK, MAX30105_INT_A_FULL_ENABLE) - - def disable_a_full(self): - # Disable the almost full interrupt (datasheet pag. 13) - self.bitmask(MAX30105_INT_ENABLE_1, MAX30105_INT_A_FULL_MASK, MAX30105_INT_A_FULL_DISABLE) - - def enable_data_rdy(self): - # Enable the new FIFO data ready interrupt (datasheet pag. 13) - self.bitmask(MAX30105_INT_ENABLE_1, MAX30105_INT_DATA_RDY_MASK, MAX30105_INT_DATA_RDY_ENABLE) - - def disable_data_rdy(self): - # Disable the new FIFO data ready interrupt (datasheet pag. 13) - self.bitmask(MAX30105_INT_ENABLE_1, MAX30105_INT_DATA_RDY_MASK, MAX30105_INT_DATA_RDY_DISABLE) - - def enable_alc_ovf(self): - # Enable the ambient light limit interrupt (datasheet pag. 13) - self.bitmask(MAX30105_INT_ENABLE_1, MAX30105_INT_ALC_OVF_MASK, MAX30105_INT_ALC_OVF_ENABLE) - - def disable_alc_ovf(self): - # Disable the ambient light limit interrupt (datasheet pag. 13) - self.bitmask(MAX30105_INT_ENABLE_1, MAX30105_INT_ALC_OVF_MASK, MAX30105_INT_ALC_OVF_DISABLE) - - def enable_prox_int(self): - # Enable the proximity interrupt (datasheet pag. 13) - self.bitmask(MAX30105_INT_ENABLE_1, MAX30105_INT_PROX_INT_MASK, MAX30105_INT_PROX_INT_ENABLE) - - def disable_prox_int(self): - # Disable the proximity interrupt (datasheet pag. 13) - self.bitmask(MAX30105_INT_ENABLE_1, MAX30105_INT_PROX_INT_MASK, MAX30105_INT_PROX_INT_DISABLE) - - def enable_die_temp_rdy(self): - # Enable the die temp. conversion finish interrupt (datasheet pag. 13) - self.bitmask(MAX30105_INT_ENABLE_2, MAX30105_INT_DIE_TEMP_RDY_MASK, MAX30105_INT_DIE_TEMP_RDY_ENABLE) - - def disable_die_temp_rdy(self): - # Disable the die temp. conversion finish interrupt (datasheet pag. 13) - self.bitmask(MAX30105_INT_ENABLE_2, MAX30105_INT_DIE_TEMP_RDY_MASK, MAX30105_INT_DIE_TEMP_RDY_DISABLE) - - # Configuration reset - - def soft_reset(self): - # When the RESET bit is set to one, all configuration, threshold, - # and data registers are reset to their power-on-state through - # a power-on reset. The RESET bit is cleared automatically back to zero - # after the reset sequence is completed. (datasheet pag. 19) - self.set_bitmask(MAX30105_MODE_CONFIG, MAX30105_RESET_MASK, MAX30105_RESET) - curr_status = -1 - while not ((curr_status & MAX30105_RESET) == 0): - sleep_ms(10) - curr_status = ord(self.i2c_read_register(MAX30105_MODE_CONFIG)) - - # Power states methods - def shutdown(self): - # 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). - self.set_bitmask(MAX30105_MODE_CONFIG, MAX30105_SHUTDOWN_MASK, MAX30105_SHUTDOWN) - - def wakeup(self): - # Pull IC out of low power mode (datasheet pg. 19) - self.set_bitmask(MAX30105_MODE_CONFIG, MAX30105_SHUTDOWN_MASK, MAX30105_WAKEUP) - - # LED Configuration - - def set_led_mode(self, LED_mode): - # Set LED mode: select which LEDs are used for sampling - # Options: RED only, RED + IR only, or ALL (datasheet pag. 19) - if LED_mode == 1: - self.set_bitmask(MAX30105_MODE_CONFIG, MAX30105_MODE_MASK, MAX30105_MODE_RED_ONLY) - elif LED_mode == 2: - self.set_bitmask(MAX30105_MODE_CONFIG, MAX30105_MODE_MASK, MAX30105_MODE_RED_IR_ONLY) - elif LED_mode == 3: - self.set_bitmask(MAX30105_MODE_CONFIG, MAX30105_MODE_MASK, MAX30105_MODE_MULTI_LED) - else: - raise ValueError('Wrong LED mode:{0}!'.format(LED_mode)) - - # Multi-LED Mode Configuration: enable the reading of the LEDs - # depending on the chosen mode - self.enable_slot(1, SLOT_RED_LED) - if LED_mode > 1: - self.enable_slot(2, SLOT_IR_LED) - if LED_mode > 2: - self.enable_slot(3, SLOT_GREEN_LED) - - # Store the LED mode used to control how many bytes to read from - # FIFO buffer in multiLED mode: a sample is made of 3 bytes - self._active_leds = LED_mode - self._multi_led_read_mode = LED_mode * 3 - - # ADC Configuration - def set_adc_range(self, ADC_range): - # ADC range: set the range of the conversion - # Options: 2048, 4096, 8192, 16384 - # Current draw: 7.81pA. 15.63pA, 31.25pA, 62.5pA per LSB. - if ADC_range == 2048: - r = MAX30105_ADC_RANGE_2048 - elif ADC_range == 4096: - r = MAX30105_ADC_RANGE_4096 - elif ADC_range == 8192: - r = MAX30105_ADC_RANGE_8192 - elif ADC_range == 16384: - r = MAX30105_ADC_RANGE_16384 - else: - raise ValueError('Wrong ADC range:{0}!'.format(ADC_range)) - - self.set_bitmask(MAX30105_PARTICLE_CONFIG, MAX30105_ADC_RANGE_MASK, r) - - # Sample Rate Configuration - def set_sample_rate(self, sample_rate): - # Sample rate: select the number of samples taken per second. - # Options: 50, 100, 200, 400, 800, 1000, 1600, 3200 - # Note: in theory, the resulting acquisition frequency for the end user - # is sampleRate/sampleAverage. However, it is worth testing it before - # assuming that the sensor can effectively sustain that frequency - # given its configuration. - if sample_rate == 50: - sr = MAX30105_SAMPLERATE_50 - elif sample_rate == 100: - sr = MAX30105_SAMPLERATE_100 - elif sample_rate == 200: - sr = MAX30105_SAMPLERATE_200 - elif sample_rate == 400: - sr = MAX30105_SAMPLERATE_400 - elif sample_rate == 800: - sr = MAX30105_SAMPLERATE_800 - elif sample_rate == 1000: - sr = MAX30105_SAMPLERATE_1000 - elif sample_rate == 1600: - sr = MAX30105_SAMPLERATE_1600 - elif sample_rate == 3200: - sr = MAX30105_SAMPLERATE_3200 - else: - raise ValueError('Wrong sample rate:{0}!'.format(sample_rate)) - - self.set_bitmask(MAX30105_PARTICLE_CONFIG, MAX30105_SAMPLERATE_MASK, sr) - - # Store the sample rate and recompute the acq. freq. - self._sample_rate = sample_rate - self.update_acquisition_frequency() - - # Pulse width Configuration - def set_pulse_width(self, pulse_width): - # Pulse width of LEDs: The longer the pulse width the longer range of - # detection. At 69us and 0.4mA it's about 2 inches, - # at 411us and 0.4mA it's about 6 inches. - if pulse_width == 69: - pw = MAX30105_PULSE_WIDTH_69 - elif pulse_width == 118: - pw = MAX30105_PULSE_WIDTH_118 - elif pulse_width == 215: - pw = MAX30105_PULSE_WIDTH_215 - elif pulse_width == 411: - pw = MAX30105_PULSE_WIDTH_411 - else: - raise ValueError('Wrong pulse width:{0}!'.format(pulse_width)) - self.set_bitmask(MAX30105_PARTICLE_CONFIG, MAX30105_PULSE_WIDTH_MASK, pw) - - # Store the pulse width - self._pulse_width = pw - - # LED Pulse Amplitude Configuration methods - def set_active_leds_amplitude(self, amplitude): - if self._active_leds > 0: - self.set_pulse_amplitude_red(amplitude) - if self._active_leds > 1: - self.set_pulse_amplitude_it(amplitude) - if self._active_leds > 2: - self.set_pulse_amplitude_green(amplitude) - - def set_pulse_amplitude_red(self, amplitude): - self.i2c_set_register(MAX30105_LED1_PULSE_AMP, amplitude) - - def set_pulse_amplitude_it(self, amplitude): - self.i2c_set_register(MAX30105_LED2_PULSE_AMP, amplitude) - - def set_pulse_amplitude_green(self, amplitude): - self.i2c_set_register(MAX30105_LED3_PULSE_AMP, amplitude) - - def set_pulse_amplitude_proximity(self, amplitude): - self.i2c_set_register(MAX30105_LED_PROX_AMP, amplitude) - - def set_proximity_threshold(self, thresh_msb): - # 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. (datasheet page 24) - self.i2c_set_register(MAX30105_PROX_INT_THRESH, thresh_msb) - - # FIFO averaged samples number Configuration - def set_fifo_average(self, number_of_samples): - # FIFO sample avg: set the number of samples to be averaged by the chip. - # Options: MAX30105_SAMPLE_AVG_1, 2, 4, 8, 16, 32 - if number_of_samples == 1: - ns = MAX30105_SAMPLE_AVG_1 - elif number_of_samples == 2: - ns = MAX30105_SAMPLE_AVG_2 - elif number_of_samples == 4: - ns = MAX30105_SAMPLE_AVG_4 - elif number_of_samples == 8: - ns = MAX30105_SAMPLE_AVG_8 - elif number_of_samples == 16: - ns = MAX30105_SAMPLE_AVG_16 - elif number_of_samples == 32: - ns = MAX30105_SAMPLE_AVG_32 - else: - raise ValueError( - 'Wrong number of samples:{0}!'.format(number_of_samples)) - self.set_bitmask(MAX30105_FIFO_CONFIG, MAX30105_SAMPLE_AVG_MASK, ns) - - # Store the number of averaged samples and recompute the acq. freq. - self._sample_avg = number_of_samples - self.update_acquisition_frequency() - - def update_acquisition_frequency(self): - if None in [self._sample_rate, self._sample_avg]: - return - else: - self._acq_frequency = self._sample_rate / self._sample_avg - from math import ceil - - # Compute the time interval to wait before taking a good measure - # (see note in setSampleRate() method) - self._acq_frequency_inv = int(ceil(1000 / self._acq_frequency)) - - def get_acquisition_frequency(self): - return self._acq_frequency - - def clear_fifo(self): - # Resets all points to start in a known state - # Datasheet page 15 recommends clearing FIFO before beginning a read - self.i2c_set_register(MAX30105_FIFO_WRITE_PTR, 0) - self.i2c_set_register(MAX30105_FIFO_OVERFLOW, 0) - self.i2c_set_register(MAX30105_FIFO_READ_PTR, 0) - - def enable_fifo_rollover(self): - # FIFO rollover: enable to allow FIFO tro wrap/roll over - self.set_bitmask(MAX30105_FIFO_CONFIG, MAX30105_ROLLOVER_MASK, MAX30105_ROLLOVER_ENABLE) - - def disable_fifo_rollover(self): - # FIFO rollover: disable to disallow FIFO tro wrap/roll over - self.set_bitmask(MAX30105_FIFO_CONFIG, MAX30105_ROLLOVER_MASK, MAX30105_ROLLOVER_DISABLE) - - def set_fifo_almost_full(self, number_of_samples): - # 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 - self.set_bitmask(MAX30105_FIFO_CONFIG, MAX30105_A_FULL_MASK, number_of_samples) - - def get_write_pointer(self): - # Read the FIFO Write Pointer from the register - wp = self.i2c_read_register(MAX30105_FIFO_WRITE_PTR) - return wp - - def get_read_pointer(self): - # Read the FIFO Read Pointer from the register - wp = self.i2c_read_register(MAX30105_FIFO_READ_PTR) - return wp - - # Die Temperature method: returns the temperature in C - def read_temperature(self): - # DIE_TEMP_RDY interrupt must be enabled - # Config die temperature register to take 1 temperature sample - self.i2c_set_register(MAX30105_DIE_TEMP_CONFIG, 0x01) - - # Poll for bit to clear, reading is then complete - reading = ord(self.i2c_read_register(MAX30105_INT_STAT_2)) - sleep_ms(100) - while (reading & MAX30105_INT_DIE_TEMP_RDY_ENABLE) > 0: - reading = ord(self.i2c_read_register(MAX30105_INT_STAT_2)) - sleep_ms(1) - - # Read die temperature register (integer) - tempInt = ord(self.i2c_read_register(MAX30105_DIE_TEMP_INT)) - # Causes the clearing of the DIE_TEMP_RDY interrupt - tempFrac = ord(self.i2c_read_register(MAX30105_DIE_TEMP_FRAC)) - - # Calculate temperature (datasheet pg. 23) - return float(tempInt) + (float(tempFrac) * 0.0625) - - def set_prox_int_tresh(self, val): - # Set the PROX_INT_THRESH (see proximity function on datasheet, pag 10) - self.i2c_set_register(MAX30105_PROX_INT_THRESH, val) - - # DeviceID and Revision methods - def read_part_id(self): - # Load the Device ID from the register - part_id = self.i2c_read_register(MAX30105_PART_ID) - return part_id - - def check_part_id(self): - # Checks the correctness of the Device ID - part_id = ord(self.read_part_id()) - return part_id == MAX_30105_EXPECTED_PART_ID - - def get_revision_id(self): - # Load the Revision ID from the register - rev_id = self.i2c_read_register(MAX30105_REVISION_ID) - return ord(rev_id) - - # Time slots management for multi-LED operation mode - def enable_slot(self, slot_number, device): - # In multi-LED mode, each sample is split into up to four time slots, - # SLOT1 through SLOT4. These control registers determine which LED is - # active in each time slot. (datasheet pag 22) - # Devices are SLOT_RED_LED or SLOT_RED_PILOT (proximity) - # Assigning a SLOT_RED_LED will pulse LED - # Assigning a SLOT_RED_PILOT will detect the proximity - if slot_number == 1: - self.bitmask(MAX30105_MULTI_LED_CONFIG_1, MAX30105_SLOT1_MASK, device) - elif slot_number == 2: - self.bitmask(MAX30105_MULTI_LED_CONFIG_1, MAX30105_SLOT2_MASK, device << 4) - elif slot_number == 3: - self.bitmask(MAX30105_MULTI_LED_CONFIG_2, MAX30105_SLOT3_MASK, device) - elif slot_number == 4: - self.bitmask(MAX30105_MULTI_LED_CONFIG_2, MAX30105_SLOT4_MASK, device << 4) - else: - raise ValueError('Wrong slot number:{0}!'.format(slot_number)) - - def disable_slots(self): - # Clear all the slots assignments - self.i2c_set_register(MAX30105_MULTI_LED_CONFIG_1, 0) - self.i2c_set_register(MAX30105_MULTI_LED_CONFIG_2, 0) - - # Low-level I2C Communication - def i2c_read_register(self, REGISTER, n_bytes=1): - self._i2c.writeto(self.i2c_address, bytearray([REGISTER])) - return self._i2c.readfrom(self.i2c_address, n_bytes) - - def i2c_set_register(self, REGISTER, VALUE): - self._i2c.writeto(self.i2c_address, bytearray([REGISTER, VALUE])) - return - - # Given a register, read it, mask it, and then set the thing - def set_bitmask(self, REGISTER, MASK, NEW_VALUES): - newCONTENTS = (ord(self.i2c_read_register(REGISTER)) & MASK) | NEW_VALUES - self.i2c_set_register(REGISTER, newCONTENTS) - return - - # Given a register, read it and mask it - def bitmask(self, reg, slotMask, thing): - originalContents = ord(self.i2c_read_register(reg)) - originalContents = originalContents & slotMask - self.i2c_set_register(reg, originalContents | thing) - - def fifo_bytes_to_int(self, fifo_bytes): - value = unpack(">i", b'\x00' + fifo_bytes) - return (value[0] & 0x3FFFF) >> self._pulse_width - - # Returns how many samples are available - def available(self): - number_of_samples = len(self.sense.red) - return number_of_samples - - # Get a new red value - def get_red(self): - # Check the sensor for new data for 250ms - if self.safe_check(250): - return self.sense.red.pop_head() - else: - # Sensor failed to find new data - return 0 - - # Get a new IR value - def get_ir(self): - # Check the sensor for new data for 250ms - if self.safe_check(250): - return self.sense.IR.pop_head() - else: - # Sensor failed to find new data - return 0 - - # Get a new green value - def get_green(self): - # Check the sensor for new data for 250ms - if self.safe_check(250): - return self.sense.green.pop_head() - else: - # Sensor failed to find new data - return 0 - - # Note: the following 3 functions are the equivalent of using 'getFIFO' - # methods of the SparkFun library - # Pops the next red value in storage (if available) - def pop_red_from_storage(self): - if len(self.sense.red) == 0: - return 0 - else: - return self.sense.red.pop() - - # Pops the next IR value in storage (if available) - def pop_ir_from_storage(self): - if len(self.sense.IR) == 0: - return 0 - else: - return self.sense.IR.pop() - - # Pops the next green value in storage (if available) - def pop_green_from_storage(self): - if len(self.sense.green) == 0: - return 0 - else: - return self.sense.green.pop() - - # (useless - for comparison purposes only) - def next_sample(self): - if self.available(): - # With respect to the SparkFun library, using a deque object - # allows us to avoid manually advancing of the tail - return True - - # Polls the sensor for new data - def check(self): - # Call continuously to poll the sensor for new data. - read_pointer = ord(self.get_read_pointer()) - write_pointer = ord(self.get_write_pointer()) - - # Do we have new data? - if read_pointer != write_pointer: - # Calculate the number of readings we need to get from sensor - number_of_samples = write_pointer - read_pointer - - # Wrap condition (return to the beginning of 32 samples) - if number_of_samples < 0: - number_of_samples += 32 - - for i in range(number_of_samples): - # Read a number of bytes equal to activeLEDs*3 (= 1 sample) - fifo_bytes = self.i2c_read_register(MAX30105_FIFO_DATA, - self._multi_led_read_mode) - - # Convert the readings from bytes to integers, depending - # on the number of active LEDs - if self._active_leds > 0: - self.sense.red.append( - self.fifo_bytes_to_int(fifo_bytes[0:3]) - ) - - if self._active_leds > 1: - self.sense.IR.append( - self.fifo_bytes_to_int(fifo_bytes[3:6]) - ) - - if self._active_leds > 2: - self.sense.green.append( - self.fifo_bytes_to_int(fifo_bytes[6:9]) - ) - - return True - - else: - return False - - # Check for new data but give up after a certain amount of time - def safe_check(self, max_time_to_check): - mark_time = ticks_ms() - while True: - if ticks_diff(ticks_ms(), mark_time) > max_time_to_check: - # Timeout reached - return False - if self.check(): - # new data found - return True - sleep_ms(1) diff --git a/package.json b/package.json new file mode 100644 index 0000000..e089a6e --- /dev/null +++ b/package.json @@ -0,0 +1,9 @@ +{ + "urls": [ + ["max30102/__init__.py", "github:n-elia/MAX30102-MicroPython-driver/max30102/__init__.py"], + ["max30102/circular_buffer.py", "github:n-elia/MAX30102-MicroPython-driver/max30102/circular_buffer.py"] + ], + "deps": [ + ], + "version": "0.4.1" +} \ No newline at end of file diff --git a/setup.py b/setup.py index 5cec4ce..6a02894 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ setup( name="micropython-max30102", - version="0.4.0", + version="0.4.1", description="MAX30102 driver for micropython.", long_description=open("README.md").read(), long_description_content_type='text/markdown', From 36126829e2929737e9d01bfa35777717e89d35f6 Mon Sep 17 00:00:00 2001 From: Nicola <62206011+n-elia@users.noreply.github.com> Date: Sun, 4 Dec 2022 16:43:40 +0100 Subject: [PATCH 04/13] Update main.py --- example/main.py | 1 + 1 file changed, 1 insertion(+) diff --git a/example/main.py b/example/main.py index 75bd4cf..d00a241 100644 --- a/example/main.py +++ b/example/main.py @@ -1,4 +1,5 @@ # main.py +# Some ports need to import 'sleep' from 'time' module from machine import sleep, SoftI2C, Pin from utime import ticks_diff, ticks_us From 9f70485c6be49ad1dbc3ebca3cd7d68276068b13 Mon Sep 17 00:00:00 2001 From: Nicola <62206011+n-elia@users.noreply.github.com> Date: Tue, 7 Feb 2023 20:27:58 +0100 Subject: [PATCH 05/13] Add module pre-compile to fix some import errors (#19) * Adds a pre-compile CI workflow that outputs a pre-compiled version of the module. * Edits some comments and updates the README. --- .github/workflows/pre-compile.yml | 83 +++++++++++++++++++++++ .github/workflows/python-publish.yml | 50 +++++++------- README.md | 99 ++++++++++++++++++---------- 3 files changed, 173 insertions(+), 59 deletions(-) create mode 100644 .github/workflows/pre-compile.yml diff --git a/.github/workflows/pre-compile.yml b/.github/workflows/pre-compile.yml new file mode 100644 index 0000000..e3edcf5 --- /dev/null +++ b/.github/workflows/pre-compile.yml @@ -0,0 +1,83 @@ +# This workflow will pre-compile the module using mpy-cross + +# To test with act: +# act -j precompile-v5 -s GITHUB_TOKEN=GH_token --artifact-server-path artifacts_v5 +# act -j precompile-v6 -s GITHUB_TOKEN=GH_token --artifact-server-path artifacts_v6 + +name: Pre-compile modules + +on: + push: + +jobs: + precompile-v5: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + with: + path: max30102 + + - uses: actions/checkout@v3 + with: + repository: 'micropython/micropython' + path: micropython + + - name: Set up Python + uses: actions/setup-python@v3 + with: + python-version: '3.8' + + # For MicroPython version <1.19, compile with mpy-cross-v5 + - name: Install mpy-cross v5 + run: | + pip install mpy-cross-v5 + + - name: Pre-compile module using mpy-cross v5 + run: | + mkdir -p precompiled/v5/max30102 + python -m mpy_cross_v5 max30102/max30102/__init__.py -o precompiled/v5/max30102/__init__.mpy + python -m mpy_cross_v5 max30102/max30102/circular_buffer.py -o precompiled/v5/max30102/circular_buffer.mpy + ls -l precompiled/v5/max30102 + + - name: Upload artifact for MicroPython version <=1.18 + uses: actions/upload-artifact@v2 + with: + name: v1.18_precompiled_module + path: precompiled/v5 + + precompile-v6: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + with: + path: max30102 + + - uses: actions/checkout@v3 + with: + repository: 'micropython/micropython' + path: micropython + + - name: Set up Python + uses: actions/setup-python@v3 + with: + python-version: '3.8' + + # For MicroPython version 1.19+, compile with mpy-cross-v6 + - name: Install mpy-cross v6 + run: | + pip install mpy-cross-v6 + + - name: Pre-compile module using mpy-cross v6 + run: | + mkdir -p precompiled/v6/max30102 + python -m mpy_cross_v6 max30102/max30102/__init__.py -o precompiled/v6/max30102/__init__.mpy + python -m mpy_cross_v6 max30102/max30102/circular_buffer.py -o precompiled/v6/max30102/circular_buffer.mpy + ls -l precompiled/v6/max30102 + + - name: Upload artifact for MicroPython version >=1.19 + uses: actions/upload-artifact@v2 + with: + name: v1.19_precompiled_module + path: precompiled/v6 \ No newline at end of file diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml index e517e2f..0f46828 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml @@ -1,37 +1,35 @@ -# This workflow will upload a Python Package using Twine when a release is created +# This workflow will upload a Python Package to PyPi using Twine when a release is created # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries -# This workflow uses actions that are not certified by GitHub. -# They are provided by a third-party and are governed by -# separate terms of service, privacy policy, and support -# documentation. - name: Upload Python Package on: release: - types: [published] + types: [ published ] jobs: - deploy: - + publish: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - name: Set up Python - uses: actions/setup-python@v2 - with: - python-version: '3.x' - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install build - pip install twine - - name: Build package - run: python -m build -s - - name: Publish package - uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 - with: - user: __token__ - password: ${{secrets.PYPI_API_TOKEN}} + - uses: actions/checkout@v2 + + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.x' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install build + pip install twine + + - name: Build package + run: python -m build -s + + - name: Publish package + uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 + with: + user: __token__ + password: ${{secrets.PYPI_API_TOKEN}} diff --git a/README.md b/README.md index e47c784..ab36449 100644 --- a/README.md +++ b/README.md @@ -11,25 +11,30 @@ feedback in the Discussions section. ## Table of contents -* [Disclaimer](#disclaimer) -* [Usage](#usage) - + [1 - Including this library into your project](#1---including-this-library-into-your-project) - - [1a - **network-enabled MicroPython ports**](#1a---network-enabled-micropython-ports) - - [1b - **manual way** (no Internet access required)](#1b---manual-way-no-internet-access-required) - + [2 - I2C setup and sensor configuration](#2---i2c-setup-and-sensor-configuration) - - [I2C connection](#i2c-connection) - - [Sensor setup](#sensor-setup) - + [3 - Data acquisition](#3---data-acquisition) - - [Read data from sensor](#read-data-from-sensor) - - [Notes on data acquisition rate](#notes-on-data-acquisition-rate) - - [Die temperature reading](#die-temperature-reading) -* [Changelog](#changelog) -* [Acknowledgements](#acknowledgements) -* [Tested platforms and known issues](#tested-platforms-and-known-issues) -* [Other useful things](#other-useful-things) - + [Realtime plot over Serial](#realtime-plot-over-serial) - + [Sensor clones](#sensor-clones) - + [Heartrate and SPO2 estimation](#heartrate-and-spo2-estimation) +# Table of contents + +- [Maxim MAX30102 MicroPython driver](#maxim-max30102-micropython-driver) + - [Table of contents](#table-of-contents) + - [Disclaimer](#disclaimer) + - [Usage](#usage) + - [1 - Including this library into your project](#1---including-this-library-into-your-project) + - [1a - **network-enabled MicroPython ports**](#1a---network-enabled-micropython-ports) + - [1b - **manual way** (no Internet access required)](#1b---manual-way-no-internet-access-required) + - [2 - I2C setup and sensor configuration](#2---i2c-setup-and-sensor-configuration) + - [I2C connection](#i2c-connection) + - [Sensor setup](#sensor-setup) + - [3 - Data acquisition](#3---data-acquisition) + - [Read data from sensor](#read-data-from-sensor) + - [Notes on data acquisition rate](#notes-on-data-acquisition-rate) + - [Die temperature reading](#die-temperature-reading) + - [Changelog](#changelog) + - [Acknowledgements](#acknowledgements) + - [Tested platforms](#tested-platforms) + - [Other useful things and troubleshooting](#other-useful-things-and-troubleshooting) + - [Realtime plot over Serial](#realtime-plot-over-serial) + - [Sensor clones](#sensor-clones) + - [Heartrate and SPO2 estimation](#heartrate-and-spo2-estimation) + - [ESP8266 module import error](#esp8266-module-import-error) ## Disclaimer @@ -46,9 +51,13 @@ A full example is provided in `/example` directory. #### 1a - **network-enabled MicroPython ports** -> Warning: in latest MicroPython releases `upip` has been deprecated in favor of [`mip`](https://docs.micropython.org/en/latest/reference/packages.html#package-management). This module is compatible with both of them. Please use the package manager included into your MicroPython version. +> Warning: in latest MicroPython releases `upip` has been deprecated in favor +> of [`mip`](https://docs.micropython.org/en/latest/reference/packages.html#package-management). This module is +> compatible +> with both of them. Please use the package manager included into your MicroPython version. -If your MicroPython version supports `mip` package manager, put these lines **after** the setup of an Internet connection: +If your MicroPython version supports `mip` package manager, put these lines **after** the setup of an Internet +connection: ```python import mip @@ -56,7 +65,8 @@ import mip mip.install("github:n-elia/MAX30102-MicroPython-driver") ``` -If your MicroPython version supports `upip` package manager, put these lines **after** the setup of an Internet connection: +If your MicroPython version supports `upip` package manager, put these lines **after** the setup of an Internet +connection: ```python import upip @@ -280,8 +290,8 @@ resolution of 0.0625ยฐC, but be aware that the accuracy is ยฑ1ยฐC. ## Changelog - v0.4.1 - - Changed the module files organization. - - Added support to `mip` package manager. + - Changed the module files organization. + - Added support to `mip` package manager. - v0.4.0 - According to some best practices discussed [here](https://forum.micropython.org/viewtopic.php?f=2&t=12508), some changes have been made. @@ -324,24 +334,26 @@ This work is a lot based on: A port of the library to MicroPython by **kandizzy** -## Tested platforms and known issues +## Tested platforms -The library has been tested on _TinyPico_ (board based on _ESP32-D4_) running 'tinypico-20210418-v1.15.bin' MicroPython -firmware, connected to a genuine Maxim 30102 breakout -board ([MAXREFDES117#](https://www.maximintegrated.com/en/design/reference-design-center/system-board/6300.html)). +- _TinyPico_ (board based on _ESP32-D4_) running 'tinypico-20210418-v1.15.bin' MicroPython firmware, connected to a + genuine Maxim 30102 breakout + board ([MAXREFDES117#](https://www.maximintegrated.com/en/design/reference-design-center/system-board/6300.html)). -Tested ([thanks to ebolisa](https://github.com/n-elia/MAX30102-MicroPython-driver/issues/4)) and working on _Raspberry Pi -Pico_ + non-Maxim breakout board. +- _Raspberry Pi + Pico_ + non-Maxim breakout board ([thanks to ebolisa](https://github.com/n-elia/MAX30102-MicroPython-driver/issues/4)) -Tested and working on _ESP32-S3_ (_Unexpected Maker TinyS3_) + non-Maxim breakout board. +- _ESP32-S3_ (_Unexpected Maker TinyS3_) running MicroPython v1.18 stable and MicroPython v1.19 stable + non-Maxim + breakout board. **I2C read issue**: as discussed in the [MicroPython forum](https://forum.micropython.org/viewtopic.php?f=2&t=12508) and in the -[GitHub Discussions section](https://github.com/n-elia/MAX30102-MicroPython-driver/discussions/5#discussioncomment-2899588), +[GitHub Discussions section](https://github.com/n-elia/MAX30102-MicroPython-driver/discussions/5#discussioncomment-2899588) +, some board/sensor combinations lead to an issue that makes the first I2C read fail. This issue can be mitigated by running an I2C scan before actually using the sensor, as shown in the provided example. -## Other useful things +## Other useful things and troubleshooting ### Realtime plot over Serial @@ -366,3 +378,24 @@ your phone camera to check), then you have to collect IR samples as red ones and If you're looking for algorithms for extracting heartrate and SPO2 from your RAW data, take a look [here](https://github.com/aromring/MAX30102_by_RF) and [here](https://github.com/kandizzy/esp32-micropython/tree/master/PPG) + + +### ESP8266 module import error + +If you get an error like this: + +``` +MemoryError: memory allocation failed,allocating 416 bytes +``` + +then your heap is too small to allocate the module. You can try to pre-compile the module using `mpy-cross` and then +import it as usual. You can either use the precompiled module provided in +the [GitHub Action artifacts](https://github.com/n-elia/MAX30102-MicroPython-driver/actions/workflows/pre-compile.yml) +or compile it +yourself. + +In the first case, you just have to replace the `max30102` folder of the module with the one provided in the artifact +archive. + +In either case, you have to choose the proper version of `mpy-cross` according to your Micropython version: for +MicroPython v.1.18 and below, you can use `mpy-cross-v5`, while for MicroPython v1.19 you have to use `mpy-cross-v6`. From 4ce03b136ec548dea4c113851b8f671054da431d Mon Sep 17 00:00:00 2001 From: Nicola <62206011+n-elia@users.noreply.github.com> Date: Tue, 7 Feb 2023 20:31:17 +0100 Subject: [PATCH 06/13] Update README.md --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ab36449..4ff8971 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ [![Upload Python Package](https://github.com/n-elia/MAX30102-MicroPython-driver/actions/workflows/python-publish.yml/badge.svg)](https://github.com/n-elia/MAX30102-MicroPython-driver/actions/workflows/python-publish.yml) +[![Pre-compile modules](https://github.com/n-elia/MAX30102-MicroPython-driver/actions/workflows/pre-compile.yml/badge.svg)](https://github.com/n-elia/MAX30102-MicroPython-driver/actions/workflows/pre-compile.yml) [![PyPI version](https://badge.fury.io/py/micropython-max30102.svg)](https://badge.fury.io/py/micropython-max30102) ![PyPI - Downloads](https://img.shields.io/pypi/dm/micropython-max30102?color=blue&label=upip%20installations) @@ -391,11 +392,12 @@ MemoryError: memory allocation failed,allocating 416 bytes then your heap is too small to allocate the module. You can try to pre-compile the module using `mpy-cross` and then import it as usual. You can either use the precompiled module provided in the [GitHub Action artifacts](https://github.com/n-elia/MAX30102-MicroPython-driver/actions/workflows/pre-compile.yml) -or compile it -yourself. +or compile ityourself. In the first case, you just have to replace the `max30102` folder of the module with the one provided in the artifact archive. In either case, you have to choose the proper version of `mpy-cross` according to your Micropython version: for MicroPython v.1.18 and below, you can use `mpy-cross-v5`, while for MicroPython v1.19 you have to use `mpy-cross-v6`. + +More information is provided into [this](https://github.com/n-elia/MAX30102-MicroPython-driver/pull/19) pull request. From 2eaf94ca9b5bd585ac113156e8e0466dc800efea Mon Sep 17 00:00:00 2001 From: Nicola <62206011+n-elia@users.noreply.github.com> Date: Mon, 13 Feb 2023 13:23:23 +0100 Subject: [PATCH 07/13] Typo fix --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4ff8971..37284b6 100644 --- a/README.md +++ b/README.md @@ -90,8 +90,9 @@ The folder tree should look as follows: โ”ฃ ๐Ÿ“œ boot.py โ”ฃ ๐Ÿ“œ main.py โ”— ๐Ÿ“‚ lib - โ”ฃ ๐Ÿ“œ __init__.py - โ”— ๐Ÿ“œ circular_buffer.py + โ”— ๐Ÿ“‚ max30102 + โ”ฃ ๐Ÿ“œ __init__.py + โ”— ๐Ÿ“œ circular_buffer.py ``` Then, import the constructor as follows: From 7f53c67ba65694a9b1ea9fb21875a9d658012183 Mon Sep 17 00:00:00 2001 From: Nicola <62206011+n-elia@users.noreply.github.com> Date: Mon, 13 Feb 2023 21:16:39 +0100 Subject: [PATCH 08/13] Fix the manual install instructions --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 37284b6..58374d5 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,7 @@ content into your microcontroller. If you prefer, you can perform a manual insta #### 1b - **manual way** (no Internet access required) To directly include the library into a MicroPython project, it's sufficient to copy `max30102/circular_buffer.py` -and `max30102/__init__.py` next to your `main.py` file, into a `lib` directory. +and `max30102/__init__.py` next to your `main.py` file, into the `lib/max30102` directory. The folder tree should look as follows: @@ -102,7 +102,7 @@ from max30102 import MAX30102 ``` To run the example in `./example` folder, copy `max30102/circular_buffer.py` and `max30102/__init__.py` into -the `./example/lib` directory. Then, upload the `./example` directory content into your microcontroller. After the +the `./example/lib/max30102` directory. Then, upload the `./example` directory content into your microcontroller. After the upload, press the reset button of your board are you're good to go. ### 2 - I2C setup and sensor configuration From fcdb14054553078a6d85550e6b7d805b7da4938c Mon Sep 17 00:00:00 2001 From: Nicola <62206011+n-elia@users.noreply.github.com> Date: Tue, 14 Feb 2023 08:48:17 +0100 Subject: [PATCH 09/13] More typos --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 58374d5..d1f1ba8 100644 --- a/README.md +++ b/README.md @@ -81,9 +81,9 @@ content into your microcontroller. If you prefer, you can perform a manual insta #### 1b - **manual way** (no Internet access required) To directly include the library into a MicroPython project, it's sufficient to copy `max30102/circular_buffer.py` -and `max30102/__init__.py` next to your `main.py` file, into the `lib/max30102` directory. +and `max30102/__init__.py`, into the `lib/max30102` directory. -The folder tree should look as follows: +The folder tree of your device should look as follows: ```text . From b9e2b93295109e88d8ec6321426dba3fa041f57b Mon Sep 17 00:00:00 2001 From: Nicola <62206011+n-elia@users.noreply.github.com> Date: Tue, 21 Feb 2023 12:04:03 +0100 Subject: [PATCH 10/13] Update README.md --- README.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/README.md b/README.md index d1f1ba8..f0d8caf 100644 --- a/README.md +++ b/README.md @@ -12,10 +12,7 @@ feedback in the Discussions section. ## Table of contents -# Table of contents - - [Maxim MAX30102 MicroPython driver](#maxim-max30102-micropython-driver) - - [Table of contents](#table-of-contents) - [Disclaimer](#disclaimer) - [Usage](#usage) - [1 - Including this library into your project](#1---including-this-library-into-your-project) From e4fc31a4c61ae47b86ddc9e02209ef5e7be2b492 Mon Sep 17 00:00:00 2001 From: Nicola <62206011+n-elia@users.noreply.github.com> Date: Wed, 7 Feb 2024 00:25:42 +0100 Subject: [PATCH 11/13] Updates the examples adding HR monitor --- {example => examples/basic_usage}/boot.py | 0 .../basic_usage}/lib/README.md | 0 {example => examples/basic_usage}/main.py | 24 ++- examples/heart_rate/README.md | 60 ++++++ examples/heart_rate/boot.py | 36 ++++ examples/heart_rate/lib/README.md | 5 + examples/heart_rate/main.py | 189 ++++++++++++++++++ 7 files changed, 313 insertions(+), 1 deletion(-) rename {example => examples/basic_usage}/boot.py (100%) rename {example => examples/basic_usage}/lib/README.md (100%) rename {example => examples/basic_usage}/main.py (78%) create mode 100644 examples/heart_rate/README.md create mode 100644 examples/heart_rate/boot.py create mode 100644 examples/heart_rate/lib/README.md create mode 100644 examples/heart_rate/main.py diff --git a/example/boot.py b/examples/basic_usage/boot.py similarity index 100% rename from example/boot.py rename to examples/basic_usage/boot.py diff --git a/example/lib/README.md b/examples/basic_usage/lib/README.md similarity index 100% rename from example/lib/README.md rename to examples/basic_usage/lib/README.md diff --git a/example/main.py b/examples/basic_usage/main.py similarity index 78% rename from example/main.py rename to examples/basic_usage/main.py index d00a241..7e4452b 100644 --- a/example/main.py +++ b/examples/basic_usage/main.py @@ -1,4 +1,26 @@ -# main.py +""" BASIC USAGE EXAMPLE +This example shows how to use the MAX30102 sensor to collect data from the RED and IR channels. + +The sensor is connected to the I2C bus, and the I2C bus is scanned to ensure that the sensor is connected. +The sensor is also checked to ensure that it is a MAX30102 or MAX30105 sensor. + +The sensor is set up with the following parameters: +- Sample rate: 400 Hz +- Averaged samples: 8 +- LED brightness: medium +- Pulse width: 411 ยตs +- Led mode: 2 (RED + IR) + +The temperature is read at the beginning of the acquisition. + +Then, in a loop the data is printed to the serial port, so that it can be plotted with a Serial Plotter. +Also the real acquisition frequency (i.e. the rate at which samples are collected from the sensor) is computed +and printed to the serial port. It differs from the sample rate, because the sensor processed the data and +averages the samples before putting them into the FIFO queue (by default, 8 samples are averaged). + +Author: n-elia +""" + # Some ports need to import 'sleep' from 'time' module from machine import sleep, SoftI2C, Pin from utime import ticks_diff, ticks_us diff --git a/examples/heart_rate/README.md b/examples/heart_rate/README.md new file mode 100644 index 0000000..1a335e1 --- /dev/null +++ b/examples/heart_rate/README.md @@ -0,0 +1,60 @@ +# Heart Rate Monitor Example + +## Overview + +The `HeartRateMonitor` class is designed to calculate heart rate from the raw sensor readings of a MAX30102 pulse oximeter and heart-rate sensor, tailored for use in a MicroPython environment on an ESP32 board. It continuously processes a stream of raw integer readings from the sensor, identifies heartbeats by detecting peaks in the signal, and calculates the heart rate based on the intervals between consecutive peaks. + +## How It Works + +- Input: The class expects individual raw sensor readings (integer values) as input, provided to it through the `add_sample` method. These readings should come from the IR or green LEDs of the MAX30102 sensor, continuously polled at a consistent rate. + +- Signal Processing: + + - **Smoothing**: The input signal is first smoothed using a moving average filter to reduce high-frequency noise. This step is crucial for accurate peak detection. + + - **Peak Detection**: The algorithm then identifies peaks in the smoothed signal using a dynamic thresholding method. A peak represents a heartbeat. + +- Heart Rate Calculation: Once peaks are identified, the class calculates the heart rate by averaging the time intervals between consecutive peaks. The result is expressed in beats per minute (BPM). + +## Parameters + +- `sample_rate` (int): Defines the rate at which samples are collected from the sensor, in samples per second (Hz). This rate should match the polling frequency of the sensor in your application. + +- `window_size` (int): Determines the number of samples over which to perform peak detection and heart rate calculation. A larger `window_size` can improve accuracy by considering more data but may also increase computation time and reduce responsiveness to changes in heart rate. Typically set based on the expected range of heart rates and the sample rate. + +- `smoothing_window` (int): Specifies the size of the moving average filter window for signal smoothing. A larger window will produce a smoother signal but may also dilute the signal's peaks, potentially affecting peak detection accuracy. The optimal size often depends on the level of noise in the signal and the sample rate. + +### Setting the Parameters + +- `sample_rate`: Set this to match the frequency at which you're polling the MAX30102 sensor. Common values are 50, 100, or 200 Hz, depending on your application's requirements for data granularity and responsiveness. + +- `window_size`: Start with a value that covers 1 to 2 seconds of data, based on your sample_rate. For example, at 100 Hz, a window size of 100 to 200 samples might be appropriate. Adjust based on testing, considering the balance between accuracy and responsiveness. + +- `smoothing_window`: Begin with a small window, such as 5 to 10 samples, and adjust based on the noise level observed in your sensor data. The goal is to smooth out high-frequency noise without significantly delaying the detection of true heartbeats. + +## Expected Input and Results + +- Input: Continuous integer readings from the MAX30102 sensor, added one at a time via the add_sample method. + +- Output: The heart rate in BPM, calculated periodically by calling the calculate_heart_rate method. This method returns None if not enough data is present to accurately calculate the heart rate. + +## Example Usage + +python +Copy code +hr_monitor = HeartRateMonitor(sample_rate=100, window_size=150, smoothing_window=10) + +```python +# Add samples in a loop (replace the sample polling with actual sensor data retrieval) +for _ in range(1000): # Example loop + sample = ... # Poll the MAX30102/5 sensor to get a new sample + hr_monitor.add_sample(sample) + # Optionally, sleep or wait based on your polling frequency + +# Calculate and print the heart rate +heart_rate = hr_monitor.calculate_heart_rate() +if heart_rate is not None: + print(f"Heart Rate: {heart_rate:.2f} BPM") +else: + print("Not enough data to calculate heart rate") +``` diff --git a/examples/heart_rate/boot.py b/examples/heart_rate/boot.py new file mode 100644 index 0000000..9b722cf --- /dev/null +++ b/examples/heart_rate/boot.py @@ -0,0 +1,36 @@ +# This file is executed on every boot (including wake-boot from deep sleep) + +def do_connect(ssid: str, password: str): + import network + wlan = network.WLAN(network.STA_IF) + wlan.active(True) + if not wlan.isconnected(): + print('connecting to network...') + wlan.connect(ssid, password) + while not wlan.isconnected(): + pass + print('network config:', wlan.ifconfig()) + + +if __name__ == '__main__': + # Put yor Wi-Fi credentials here + my_ssid = "my_ssid" + my_pass = "my_password" + + # Check if the module is available in memory + try: + from max30102 import MAX30102 + except ImportError as e: + # Module not available. Try to connect to Internet to download it. + print(f"Import error: {e}") + print("Trying to connect to the Internet to download the module.") + do_connect(my_ssid, my_pass) + try: + # Try to leverage upip package manager to download the module. + import upip + upip.install("micropython-max30102") + except ImportError: + # upip not available. Try to leverage mip package manager to download the module. + print("upip not available in this port. Trying with mip.") + import mip + mip.install("github:n-elia/MAX30102-MicroPython-driver") diff --git a/examples/heart_rate/lib/README.md b/examples/heart_rate/lib/README.md new file mode 100644 index 0000000..37013b4 --- /dev/null +++ b/examples/heart_rate/lib/README.md @@ -0,0 +1,5 @@ +# Note + +To manually install the library, copy the files `max30102/circular_buffer.py`, `max30102/max30102.py` and past them here. + +Then, load the content of `example` directory into the board. diff --git a/examples/heart_rate/main.py b/examples/heart_rate/main.py new file mode 100644 index 0000000..ff8cbe1 --- /dev/null +++ b/examples/heart_rate/main.py @@ -0,0 +1,189 @@ +# main.py +# Some ports need to import 'sleep' from 'time' module +from machine import sleep, SoftI2C, Pin +from utime import ticks_diff, ticks_us, ticks_ms + +from max30102 import MAX30102, MAX30105_PULSE_AMP_MEDIUM + + +class HeartRateMonitor: + """A simple heart rate monitor that uses a moving window to smooth the signal and find peaks.""" + + def __init__(self, sample_rate=100, window_size=10, smoothing_window=5): + self.sample_rate = sample_rate + self.window_size = window_size + self.smoothing_window = smoothing_window + self.samples = [] + self.timestamps = [] + self.filtered_samples = [] + + def add_sample(self, sample): + """Add a new sample to the monitor.""" + timestamp = ticks_ms() + self.samples.append(sample) + self.timestamps.append(timestamp) + + # Apply smoothing + if len(self.samples) >= self.smoothing_window: + smoothed_sample = ( + sum(self.samples[-self.smoothing_window :]) / self.smoothing_window + ) + self.filtered_samples.append(smoothed_sample) + else: + self.filtered_samples.append(sample) + + # Maintain the size of samples and timestamps + if len(self.samples) > self.window_size: + self.samples.pop(0) + self.timestamps.pop(0) + self.filtered_samples.pop(0) + + def find_peaks(self): + """Find peaks in the filtered samples.""" + peaks = [] + + if len(self.filtered_samples) < 3: # Need at least three samples to find a peak + return peaks + + # Calculate dynamic threshold based on the min and max of the recent window of filtered samples + recent_samples = self.filtered_samples[-self.window_size :] + min_val = min(recent_samples) + max_val = max(recent_samples) + threshold = ( + min_val + (max_val - min_val) * 0.5 + ) # 50% between min and max as a threshold + + for i in range(1, len(self.filtered_samples) - 1): + if ( + self.filtered_samples[i] > threshold + and self.filtered_samples[i - 1] < self.filtered_samples[i] + and self.filtered_samples[i] > self.filtered_samples[i + 1] + ): + peak_time = self.timestamps[i] + peaks.append((peak_time, self.filtered_samples[i])) + + return peaks + + def calculate_heart_rate(self): + """Calculate the heart rate in beats per minute (BPM).""" + peaks = self.find_peaks() + + if len(peaks) < 2: + return None # Not enough peaks to calculate heart rate + + # Calculate the average interval between peaks in milliseconds + intervals = [] + for i in range(1, len(peaks)): + interval = ticks_diff(peaks[i][0], peaks[i - 1][0]) + intervals.append(interval) + + average_interval = sum(intervals) / len(intervals) + + # Convert intervals to heart rate in beats per minute (BPM) + heart_rate = ( + 60000 / average_interval + ) # 60 seconds per minute * 1000 ms per second + + return heart_rate + + +def main(): + # I2C software instance + i2c = SoftI2C( + sda=Pin(8), # Here, use your I2C SDA pin + scl=Pin(9), # Here, use your I2C SCL pin + freq=400000, + ) # Fast: 400kHz, slow: 100kHz + + # Examples of working I2C configurations: + # Board | SDA pin | SCL pin + # ------------------------------------------ + # ESP32 D1 Mini | 22 | 21 + # TinyPico ESP32 | 21 | 22 + # Raspberry Pi Pico | 16 | 17 + # TinyS3 | 8 | 9 + + # Sensor instance + sensor = MAX30102(i2c=i2c) # An I2C instance is required + + # Scan I2C bus to ensure that the sensor is connected + if sensor.i2c_address not in i2c.scan(): + print("Sensor not found.") + return + elif not (sensor.check_part_id()): + # Check that the targeted sensor is compatible + print("I2C device ID not corresponding to MAX30102 or MAX30105.") + return + else: + print("Sensor connected and recognized.") + + # Load the default configuration + print("Setting up sensor with default configuration.", "\n") + sensor.setup_sensor() + + # Set the sample rate to 400: 400 samples/s are collected by the sensor + sensor_sample_rate = 400 + sensor.set_sample_rate(sensor_sample_rate) + + # Set the number of samples to be averaged per each reading + sensor_fifo_average = 8 + sensor.set_fifo_average(sensor_fifo_average) + + # Set LED brightness to a medium value + sensor.set_active_leds_amplitude(MAX30105_PULSE_AMP_MEDIUM) + + # Expected acquisition rate: 400 Hz / 8 = 50 Hz + actual_acquisition_rate = int(sensor_sample_rate / sensor_fifo_average) + + sleep(1) + + print( + "Starting data acquisition from RED & IR registers...", + "press Ctrl+C to stop.", + "\n", + ) + sleep(1) + + # Initialize the heart rate monitor + hr_monitor = HeartRateMonitor( + # Select a sample rate that matches the sensor's acquisition rate + sample_rate=actual_acquisition_rate, + # Select a significant window size to calculate the heart rate (2-5 seconds) + window_size=int(actual_acquisition_rate * 3), + ) + + # Setup to calculate the heart rate every 2 seconds + hr_compute_interval = 2 # seconds + ref_time = ticks_ms() # Reference time + + while True: + # The check() method has to be continuously polled, to check if + # there are new readings into the sensor's FIFO queue. When new + # readings are available, this function will put them into the storage. + sensor.check() + + # Check if the storage contains available samples + if sensor.available(): + # Access the storage FIFO and gather the readings (integers) + red_reading = sensor.pop_red_from_storage() + ir_reading = sensor.pop_ir_from_storage() + + # Add the IR reading to the heart rate monitor + # Note: based on the skin color, the red, IR or green LED can be used + # to calculate the heart rate with more accuracy. + hr_monitor.add_sample(ir_reading) + + # Periodically calculate the heart rate every `hr_compute_interval` seconds + if ticks_diff(ticks_ms(), ref_time) / 1000 > hr_compute_interval: + # Calculate the heart rate + heart_rate = hr_monitor.calculate_heart_rate() + if heart_rate is not None: + print("Heart Rate: {:.0f} BPM".format(heart_rate)) + else: + print("Not enough data to calculate heart rate") + # Reset the reference time + ref_time = ticks_ms() + + +if __name__ == "__main__": + main() From 2b9a810087802cd8ac9af9b24a8941f6451e0860 Mon Sep 17 00:00:00 2001 From: Nicola <62206011+n-elia@users.noreply.github.com> Date: Tue, 12 Mar 2024 20:01:55 +0100 Subject: [PATCH 12/13] Updates docs and issues a new release --- README.md | 14 +++++++++----- package.json | 2 +- setup.py | 2 +- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index f0d8caf..45b4b8e 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ Please do not rely on it for medical purposes or professional usage. Driver usage is quite straightforward. You just need to import the library, and to set up a `SoftI2C` instance. -A full example is provided in `/example` directory. +A full example is provided in `/examples/basic_usage` directory. ### 1 - Including this library into your project @@ -98,8 +98,8 @@ Then, import the constructor as follows: from max30102 import MAX30102 ``` -To run the example in `./example` folder, copy `max30102/circular_buffer.py` and `max30102/__init__.py` into -the `./example/lib/max30102` directory. Then, upload the `./example` directory content into your microcontroller. After the +To run the example in `./examples/basic_usage` folder, copy `max30102/circular_buffer.py` and `max30102/__init__.py` into +the `./examples/basic_usage/lib/max30102` directory. Then, upload the `./examples/basic_usage` directory content into your microcontroller. After the upload, press the reset button of your board are you're good to go. ### 2 - I2C setup and sensor configuration @@ -288,6 +288,9 @@ resolution of 0.0625ยฐC, but be aware that the accuracy is ยฑ1ยฐC. ## Changelog +- v0.4.2 + - Added an heartrate estimation example. + - Issued a new release to update the PyPi docs. - v0.4.1 - Changed the module files organization. - Added support to `mip` package manager. @@ -356,7 +359,7 @@ running an I2C scan before actually using the sensor, as shown in the provided e ### Realtime plot over Serial -The example proposed in this repository ([main.py](./example/main.py)) contains a print statement in a CSV-like +The example proposed in this repository ([main.py](./examples/basic_usage/main.py)) contains a print statement in a CSV-like format: `print(red_reading, ",", IR_reading)`. If you open Arduino IDE and connect your board, then you will be able to open the *serial plotter* (Ctrl+Maiusc+L) and see a real-time plot of your readings (need some help? take a look [here](https://learn.sparkfun.com/tutorials/max30105-particle-and-pulse-ox-sensor-hookup-guide/all)). @@ -376,8 +379,9 @@ your phone camera to check), then you have to collect IR samples as red ones and If you're looking for algorithms for extracting heartrate and SPO2 from your RAW data, take a look [here](https://github.com/aromring/MAX30102_by_RF) -and [here](https://github.com/kandizzy/esp32-micropython/tree/master/PPG) +and [here](https://github.com/kandizzy/esp32-micropython/tree/master/PPG). +A basic example of heartrate detection is also available in `./examples/heart_rate`. ### ESP8266 module import error diff --git a/package.json b/package.json index e089a6e..2198371 100644 --- a/package.json +++ b/package.json @@ -5,5 +5,5 @@ ], "deps": [ ], - "version": "0.4.1" + "version": "0.4.2" } \ No newline at end of file diff --git a/setup.py b/setup.py index 6a02894..31a80ec 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ setup( name="micropython-max30102", - version="0.4.1", + version="0.4.2", description="MAX30102 driver for micropython.", long_description=open("README.md").read(), long_description_content_type='text/markdown', From f1e7b11aa352a8d10be0f1ea468a4a873d7f391d Mon Sep 17 00:00:00 2001 From: Nicola <62206011+n-elia@users.noreply.github.com> Date: Wed, 10 Apr 2024 18:24:18 +0200 Subject: [PATCH 13/13] Fixes some typos --- examples/heart_rate/README.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/examples/heart_rate/README.md b/examples/heart_rate/README.md index 1a335e1..f431420 100644 --- a/examples/heart_rate/README.md +++ b/examples/heart_rate/README.md @@ -40,11 +40,17 @@ The `HeartRateMonitor` class is designed to calculate heart rate from the raw se ## Example Usage -python -Copy code -hr_monitor = HeartRateMonitor(sample_rate=100, window_size=150, smoothing_window=10) +For a complete example, see `./main.py`. ```python +# Initialize the heart rate monitor +hr_monitor = HeartRateMonitor( + # Select a sample rate that matches the sensor's acquisition rate + sample_rate=actual_acquisition_rate, + # Select a significant window size to calculate the heart rate (2-5 seconds) + window_size=int(actual_acquisition_rate * 3), +) + # Add samples in a loop (replace the sample polling with actual sensor data retrieval) for _ in range(1000): # Example loop sample = ... # Poll the MAX30102/5 sensor to get a new sample