Raspberry Piと心拍センサ(MAX30102)で脈を見てみよう

(
2019.05.02 追記: github側で「redとirの読み出しがごっちゃだよ」と指摘があったので修正しました．
影響範囲は`max30102.py`の`read_fifo()`部分と，「グラフ表示してみる」全般です．
)

健康，気になりませんか?

いまどきはApple WatchやFitbitで心拍数(BPM)をモニタできますよね．当然アプリでも数字は確認できますが，生の波形を見たくないですか?私は見たいです．

ということで見てみましょう．この記事では，「i2c対応の心拍センサを使い，(1)Raspberry Pi上のPythonから制御を行い，(2)心拍波形を読み取って保存し，(3)グラフを作る」を目標にします．

# 使うハードウェア
* Raspberry Pi (ここでは，Raspberry Pi Zero Wを使いました)  
USB電源やキーボード・モニタ・ネットワーク等はよしなに
* 心拍センサMAXREFDES117#  
販売ページ: https://www.switch-science.com/catalog/3208/
* ジャンパ線  
心拍センサとRaspberry Piの接続に使います(はんだ付けが必要です．今回はピンヘッダを生やしてブレッドボードに刺しました)

![IMG_4065.jpg](https://qiita-image-store.s3.amazonaws.com/0/99470/8dd355e3-2662-8ffa-6c76-42345b6c2f0c.jpeg)
↑こんな感じで動かしてました

---------------------------

# まずは公式の情報を確認
Switch Scienceの通販ページでは，資料としてサンプルコードへのリンクが貼られています．
(ここ: https://www.maximintegrated.com/en/design/reference-design-center/system-board/6300.html/tb_tab2 )
mbedおよびArduinoのサンプルコードが載っていますが，Raspberry Pi向けはありません．
i2c対応だからADCが無いRaspberry Piでもデータを取れるじゃんと思ったのですが……

ないなら書こうということでArduinoのコードを見て，それをRaspberry Piに移植してみます．

## Arduino向けコードを見る
↑のサンプルコードのうち`Arduino Quick Start Guide`を見ると，Arduino向けコードとして https://github.com/MaximIntegratedRefDesTeam/RD117_ARDUINO/ がリンクされています．
サンプルコードを動かすと，シリアルモニタに以下の情報が流れます．

* 心拍センサのLED(赤色，IR)読み取り値
* 心拍数とSpO2およびその値が正しいかどうか(valid)

サンプルコードの`RD117_ARDUINO.ino`を見ると，Arduinoでお馴染み`setup()`→`loop()`がありますね．
`setup()`ではシリアルモニタの設定と心拍センサの状態リセット・初期設定が，`loop()`ではセンサからの値の読み取りと心拍数/SpO2の計算が，それぞれ行われます．

心拍センサ関連の関数は`max30102.cpp`に実体が入っており，以下があります．

* `bool maxim_max30102_init()`: デバイスの初期化
* `bool maxim_max30102_read_fifo()` (引数違いで2種): データの読み出し
* `bool maxim_max30102_write_reg()`: レジスタに書き込み
* `bool maxim_max30102_read_reg()`: レジスタから読み出し
* `bool maxim_max30102_reset()`: デバイスのリセット

これらはi2c通信で情報をやり取りしており，`SoftI2CMaster.h`でソフト的に実装されているようです．

一方，心拍数の計算とSpO2の計算は`algorithm.cpp`にありますが，生データを得るのが目的のこの記事ではカバーしません．

# いざRaspberry Piで動かす
## Raspberry Piのi2c機能の有効化
このあたりの話は[RaspberryPiではじめてのI2C通信〜温度計測編〜](https://qiita.com/kamujun/items/51f85339bfd582b27752)を参考にしました．
この記事ではRaspbian stretchで動作確認しています．

i2c機能を有効化します．以下のコマンドを実行します．

```shell
$ sudo raspi-config
```

設定画面が呼び出されるので，`5 Interfacing Options`を選択します．

<img width="325" alt="raspi-config.png" src="https://qiita-image-store.s3.amazonaws.com/0/99470/aedf10dd-01db-8832-30f8-7338a27354c4.png">

`P5 I2C`を選びます．

<img width="325" alt="interfacing-options.png" src="https://qiita-image-store.s3.amazonaws.com/0/99470/909939ce-0873-d319-2b20-b8e2e63d3de1.png">

`Would you like the ARM I2C interface to be enabled?`と聞かれるので`Yes`と答えると有効化されます(自分の場合は再起動の必要はありませんでした)．

<img width="325" alt="i2c.png" src="https://qiita-image-store.s3.amazonaws.com/0/99470/f3d81227-875d-d5e7-7a7a-cdf0293b502b.png">

## i2c関連ツールの導入
関連のツールとPython用ライブラリを入れます．

```shell
$ sudo apt-get update
$ sudo apt-get install i2c-tools python-smbus
```

今回はGPIOピンも利用するため，以下のコマンドを実行して必要なライブラリを入れておきます．

```shell
$ sudo pip install rpi.gpio
```

## 心拍センサの接続と最低限の動作確認
GPIOピンに対し以下の図のようにセンサを接続しました．

![pizero-sensor-connection.png](https://qiita-image-store.s3.amazonaws.com/0/99470/6474710d-bb05-8931-d6e8-41546af5a180.png)

(i2cは電源/GND/データ線2本の4線で通信するようですが，この心拍センサでは割り込みピンが出ています．
このピンの状態でデータ取得の可否が通知されます．今回はこのピンを7番に接続しました．)

接続が終わったら，ターミナルからセンサが見えているか確認してみましょう．
`$ sudo i2cdetect -y 1`すると次のような表示が得られるはずです．今回は`0x57`がデバイスのアドレスとして設定されています．

```shell
$ sudo i2cdetect -y 1
     0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f
00:          -- -- -- -- -- -- -- -- -- -- -- -- --
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
50: -- -- -- -- -- -- -- 57 -- -- -- -- -- -- -- --
60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
70: -- -- -- -- -- -- -- --
```

## Pythonからi2c通信を行って赤色/IR LEDの生データを取得する
書いたコードをこちらに置いておきます: https://github.com/vrano714/max30102-tutorial-raspberrypi

### Arduinoコードの移植1: 最低限のデータ読み書き
i2cでのデータ読み/書きは`smbus`で可能なので，`max30102.cpp`から移植するのは`init()`と`read_fifo()`および`reset()`になります．
とはいってもレジスタに値を送りつけるだけなので，このままベタに移植していきます．
各値の意味はデータシートを参照( https://www.mouser.jp/datasheet/2/256/maximintegratedproducts_MAX30102%20DS-1179649.pdf )


```max30102.py
import smbus

# レジスタアドレス省略

class MAX30102():
    # setup()相当の部分
    def __init__(self, channel=1, address=0x57, gpio_pin=7):
        # Raspberry Piでi2cを行う各種設定
        self.address = address
        self.channel = channel
        # i2cのセットアップ
        self.bus = smbus.SMBus(self.channel)

        # (一部省略)

        self.reset()

        sleep(1) # wait 1 sec

        # 割り込みレジスタを読み捨て(この心拍センサは割り込みレジスタを読むと割り込み状態をクリアする)
        reg_data = self.bus.read_i2c_block_data(self.address, REG_INTR_STATUS_1, 1)

        self.setup()

    def reset(self):
        self.bus.write_i2c_block_data(self.address, REG_MODE_CONFIG, [0x40])

    def setup(self, led_mode=0x03):
        # INTR setting
        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])

        # 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])
        # 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])

    # 赤色/IRのLEDのデータを読むのはこれ
    def read_fifo(self):
        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
```

先程確認した`0x57`をデフォルトのアドレスとして`__init()__`に入れておきました．また，`channel`はRaspberry Piのi2cバス番号らしい?です(詳しい方〜〜)．


### Arduinoコードの移植2: 割り込みを見つつデータを継続的に取得できるようにする
上でも書きましたが，今回使っている心拍センサでは割り込みでデータの取得可否を通知します．
これはサンプルコードでも`setup()`でピンを指定し，`loop()`内でピンの状態を監視してから値の取得を行うよう実装されているので，今回のコードでもその通りにします．
`read_sequential()`で指定した量のデータを取得できるようにしました．

```cpp:RD117_ARDUINO.ino
// setup()内
pinMode(10, INPUT);  //pin D10 connects to the interrupt output pin of the MAX30102

// loop()内の各所
while(digitalRead(10)==1);  //wait until the interrupt pin asserts
```

```max30102.py
# GPIOを触るためにライブラリを読み込み
import RPi.GPIO as GPIO

class MAX30102():
    # setup()相当の部分
    def __init__(self, channel=1, address=0x57, gpio_pin=7):
        # (一部省略)
        self.interrupt = gpio_pin

        # GPIO.BOARD: ボード上ピン番号(=物理的な番号)で指定
        GPIO.setmode(GPIO.BOARD)
        # デフォルトでは7番ピンが入力ピンとして設定される
        GPIO.setup(self.interrupt, GPIO.IN)

    # 途中省略

    # 指定した量のデータを読み込む
    def read_sequential(self, amount=100):
        red_buf = []
        ir_buf = []
        for i in range(amount):
            while(GPIO.input(self.interrupt) == 1):
                # 割り込みピンがLOW(0)になるまで待つ: 待っている間は何もしない
                pass
            red, ir = self.read_fifo()
            red_buf.append(red)
            ir_buf.append(ir)

        return red_buf, ir_buf
```

最終的に出来上がったクラスは↑のソースコードに入っています．

## 生データを読んでダンプする
実際に値を読んでみます．`hrdump.py`を書いてみました．やっていることはシンプルで，`read_sequential()`で1000サンプル取得して，ファイルに保存するだけです．
指が乗っていなくても値が出るので，実行前からセンサに指を乗せておくほうがよいかもしれません
(データシートには距離で割り込みをかけるような記載もありましたが，今回は実装していません)．

```hrdump.py
import max30102

m = max30102.MAX30102()

red, ir = m.read_sequential(1000)
with open("./red.log", "w") as f:
    for r in red:
        f.write("{0}\n".format(r))
with open("./ir.log", "w") as f:
    for r in ir:
        f.write("{0}\n".format(r))

m.shutdown()
```

## グラフ表示してみる
グラフの生成は`red.log`と`ir.log`をscp等で手元(mac)に持ってきて行いました．Raspberry Pi上でやる場合はmatplotlib & numpyを導入して行うことになります(CLI上で行う場合はmatplotlibをGUI無しで動作させるための書き換えが必要です)．

```makegraph.py
import matplotlib.pyplot as plt
import numpy as np

red = []
with open("./red.log", "r") as f:
    for r in f:
        red.append(int(r))

ir = []
with open("./ir.log", "r") as f:
    for r in f:
        ir.append(int(r))

# 横軸用
x = np.arange(len(red))

fig = plt.figure()
ax = fig.add_subplot(111)

# 赤色LED
ax.plot(x, red, c="red", label="RED LED")
# IR LED
ax.plot(x, ir, c="orange", label="IR LED")

# 表示範囲を調整
ax.set_ylim(100000, 150000)

# 凡例表示
ax.legend(loc="best")

# 画像を表示
plt.show()
```

そうしてできたグラフがこちらです．

![Figure_0_fix.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99470/53d5ad59-dc46-2b4b-cda4-56cec3af6695.png)


なんだかわからないので，IR LEDの値に下駄を履かせた上で`ax.set_ylim()`を調整してみます．

```diff

# IR LED
-ax.plot(x, ir, c="orange", label="IR LED")
+ax.plot(x, np.array(ir)-20000, c="orange", label="IR LED (shifted)")

# 表示範囲を調整
-ax.set_ylim(100000, 150000)
+ax.set_ylim(122000, 126000)
```

![Figure_1_fix.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99470/517c9010-aba3-ac35-bbc7-5c05a97ebe54.png)


それっぽい波形が見えてきました．比較的きれいそうな400-600サンプル目に絞ってみます．

```diff
# 表示範囲を調整
 ax.set_ylim(122000, 126000)
+ax.set_xlim(400, 600)
```
![Figure_2_fix.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99470/186c2a3e-4f28-040a-5ca8-406e245a261b.png)



それっぽくなりました．赤色LEDのほうはゴチャついていますが，IR LEDはきれいに値が変動しています．
現在(=サンプルコード)は秒間25サンプル取得なので，落ちるスパイクの感覚的に60BPM程度の心拍数だと思われます．

# おわりに
この記事ではi2c接続の心拍センサをRaspberry Pi + Pythonで動かして，値を保存し，グラフにプロットするまでを行いました．
心拍数の計算等もできるようになったら投稿したいですね．

# 補足
実はRaspberry Pi向けのコードを作っている人がいますので参考までに紹介しておきます．このコードでは一通りの機能が利用できるよう設計されています．
こちら: https://github.com/oscaratnc/MAX30102-in-raspberry-pi-