MAX30102で心拍数と血中酸素濃度を測定する - Raspberry Pi編 -

## 概要
* 新型コロナの影響でヘルスケア関連のデバイスの需要が高まっている
* maxim integrated社から出ているMAX3010xシリーズの最新版を試してみた
* まず開発キット(MAXREFDES117)を入手し、動作を確かめてみる

開発キット(MAXREFDES117)
![ios__________-1.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/701445/47e08282-c0a9-6fcb-b472-05e31026e5b1.jpeg)
![ios__________.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/701445/19bff5b3-7b44-471a-962a-2af3d962e7a9.jpeg)

## 用意するもの
* Raspberry Pi4 (3でもおそらく大丈夫)
* Raspberry Pi用のケース
* Raspberry Pi用のヒートシンク
* Raspberry Pi4 対応 電源セット(5V 3.0A)
* MAX30102開発キット(MAXREFDES117)  
  https://www.maximintegrated.com/jp/design/reference-design-center/system-board/6300.html
* ブレッドボード  
* ジャンパーワイヤー各種

## 環境構築
* Raspberry PiのOSをセットアップし、I2Cを有効化する  
https://www.raspberrypi.org/downloads/  
https://www.indoorcorgielec.com/resources/raspberry-pi/raspberry-pi-i2c/  

## 組み立て
Raspberry Piと開発キット(MAXREFDES117)を以下のように接続する
![スクリーンショット 2021-08-22 10.14.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/701445/08d422fe-72b9-07cf-ed6a-1631394e4b15.png)
※Raspberry Piのピン配置は割愛します。

## プログラミング
* まず動作確認のため、既存のリポジトリを使って動かしてみた  
https://github.com/vrano714/max30102-tutorial-raspberrypi

メインプログラム(GitHubより転載)
maxim integrated社の公式サイトにArduino用のサンプルコードがある。それをもとに開発された模様。
（サンプルコードのダウンロードは公式サイトにアカウント作成、ユーザー登録を行う必要がある）

```Python:/home/pi/guest/max30102.py
# -*-coding:utf-8-*-

# this code is currently for python 2.7
from __future__ import print_function
from time import sleep

import RPi.GPIO as GPIO
import smbus

# i2c address-es
# not required?
I2C_WRITE_ADDR = 0xAE
I2C_READ_ADDR = 0xAF

# register address-es
REG_INTR_STATUS_1 = 0x00
REG_INTR_STATUS_2 = 0x01

REG_INTR_ENABLE_1 = 0x02
REG_INTR_ENABLE_2 = 0x03

REG_FIFO_WR_PTR = 0x04
REG_OVF_COUNTER = 0x05
REG_FIFO_RD_PTR = 0x06
REG_FIFO_DATA = 0x07
REG_FIFO_CONFIG = 0x08

REG_MODE_CONFIG = 0x09
REG_SPO2_CONFIG = 0x0A
REG_LED1_PA = 0x0C

REG_LED2_PA = 0x0D
REG_PILOT_PA = 0x10
REG_MULTI_LED_CTRL1 = 0x11
REG_MULTI_LED_CTRL2 = 0x12

REG_TEMP_INTR = 0x1F
REG_TEMP_FRAC = 0x20
REG_TEMP_CONFIG = 0x21
REG_PROX_INT_THRESH = 0x30
REG_REV_ID = 0xFE
REG_PART_ID = 0xFF

# currently not used
MAX_BRIGHTNESS = 255


class MAX30102():
    # by default, this assumes that physical pin 7 (GPIO 4) is used as interrupt
    # by default, this assumes that the device is at 0x57 on channel 1
    def __init__(self, channel=1, address=0x57, gpio_pin=7):
        print("Channel: {0}, address: 0x{1:x}".format(channel, address))
        self.address = address
        self.channel = channel
        self.bus = smbus.SMBus(self.channel)
        self.interrupt = gpio_pin

        # set gpio mode
        GPIO.setmode(GPIO.BOARD)
        GPIO.setup(self.interrupt, GPIO.IN)

        self.reset()

        sleep(1)  # wait 1 sec

        # read & clear interrupt register (read 1 byte)
        reg_data = self.bus.read_i2c_block_data(self.address, REG_INTR_STATUS_1, 1)
        # print("[SETUP] reset complete with interrupt register0: {0}".format(reg_data))
        self.setup()
        # print("[SETUP] setup complete")

    def shutdown(self):
        """
        Shutdown the device.
        """
        self.bus.write_i2c_block_data(self.address, REG_MODE_CONFIG, [0x80])

    def reset(self):
        """
        Reset the device, this will clear all settings,
        so after running this, run setup() again.
        """
        self.bus.write_i2c_block_data(self.address, REG_MODE_CONFIG, [0x40])

    def setup(self, led_mode=0x03):
        """
        This will setup the device with the values written in sample Arduino code.
        """
        # INTR setting
        # 0xc0 : A_FULL_EN and PPG_RDY_EN = Interrupt will be triggered when
        # fifo almost full & new fifo data ready
        self.bus.write_i2c_block_data(self.address, REG_INTR_ENABLE_1, [0xc0])
        self.bus.write_i2c_block_data(self.address, REG_INTR_ENABLE_2, [0x00])

        # FIFO_WR_PTR[4:0]
        self.bus.write_i2c_block_data(self.address, REG_FIFO_WR_PTR, [0x00])
        # OVF_COUNTER[4:0]
        self.bus.write_i2c_block_data(self.address, REG_OVF_COUNTER, [0x00])
        # FIFO_RD_PTR[4:0]
        self.bus.write_i2c_block_data(self.address, REG_FIFO_RD_PTR, [0x00])

        # 0b 0100 1111
        # sample avg = 4, fifo rollover = false, fifo almost full = 17
        self.bus.write_i2c_block_data(self.address, REG_FIFO_CONFIG, [0x4f])

        # 0x02 for read-only, 0x03 for SpO2 mode, 0x07 multimode LED
        self.bus.write_i2c_block_data(self.address, REG_MODE_CONFIG, [led_mode])
        # 0b 0010 0111
        # SPO2_ADC range = 4096nA, SPO2 sample rate = 100Hz, LED pulse-width = 411uS
        self.bus.write_i2c_block_data(self.address, REG_SPO2_CONFIG, [0x27])

        # choose value for ~7mA for LED1
        self.bus.write_i2c_block_data(self.address, REG_LED1_PA, [0x24])
        # choose value for ~7mA for LED2
        self.bus.write_i2c_block_data(self.address, REG_LED2_PA, [0x24])
        # choose value fro ~25mA for Pilot LED
        self.bus.write_i2c_block_data(self.address, REG_PILOT_PA, [0x7f])

    # this won't validate the arguments!
    # use when changing the values from default
    def set_config(self, reg, value):
        self.bus.write_i2c_block_data(self.address, reg, value)

    def read_fifo(self):
        """
        This function will read the data register.
        """
        red_led = None
        ir_led = None

        # read 1 byte from registers (values are discarded)
        reg_INTR1 = self.bus.read_i2c_block_data(self.address, REG_INTR_STATUS_1, 1)
        reg_INTR2 = self.bus.read_i2c_block_data(self.address, REG_INTR_STATUS_2, 1)

        # read 6-byte data from the device
        d = self.bus.read_i2c_block_data(self.address, REG_FIFO_DATA, 6)

        # mask MSB [23:18]
        red_led = (d[0] << 16 | d[1] << 8 | d[2]) & 0x03FFFF
        ir_led = (d[3] << 16 | d[4] << 8 | d[5]) & 0x03FFFF

        return red_led, ir_led

    def read_sequential(self, amount=100):
        """
        This function will read the red-led and ir-led `amount` times.
        This works as blocking function.
        """
        red_buf = []
        ir_buf = []
        for i in range(amount):
            while(GPIO.input(self.interrupt) == 1):
                # wait for interrupt signal, which means the data is available
                # do nothing here
                pass

            red, ir = self.read_fifo()

            red_buf.append(red)
            ir_buf.append(ir)

        return red_buf, ir_buf
```


## 検証
* 心拍数と血中酸素濃度(SpO2)が同時に取得できている
* 発光面に指を触れると読み取りエラーとなる  
* ジャンパーワイヤを曲げてスペーサーとし、触れないようにしてみると安定した数値が取得できた  
* 人差し指以外でも値は取得できたが、指の置き方でエラーになりやすい
* 周囲の明るさの影響を受けるので、パルスオキシメーターのようなカバーリングは必要

![ezgif-6-1262d3f1c022.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/701445/e4918c30-6dad-9b2a-c8cd-dd8413529849.gif)
![ios_______.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/701445/2acf2de7-993a-2ab5-464c-7a5fcb02a45f.jpeg)

## 次回に向けて
* Arduino nanoとOLED液晶を使い、小型化・表示装置を搭載した単体のデバイスにできないか検証する