Raspberry Piで心拍数を監視して心臓が止まりそうになったらアラート音を鳴らす

  • 36
    いいね
  • 2
    コメント

概要

hr_alart_image.png
心拍数を送信できるANT+デバイス(Garmin vivosmart HR J)から、Raspberry Piに心拍数を送信し、異常値であれば音を鳴らすようにしました。

きっかけ(ちょっと切実)

診察でブルガダ症候群の疑いを指摘され、睡眠中に突然死のリスクがあると言われる。しかし服薬でどうにかなるものではなく、植込み型除細動器(ICD)の埋め込みの基準にも該当しないため、現状ではこのまま生きていくしかない。
睡眠中に突然死する場合は、心室細動が起こって血圧が下がって寝たまま意識を失ってしまい、自分ではどうにもならないとのことなので、とりあえず家族に気づいてもらえるようにしたい。

fitbitを試す

ちょうど「心臓が止まったらSNSに「死にました」と投稿する - Qiita」を読んだところだったので、fitbit alta HRを買って、Raspberry Piを使い、fitbit API経由で心拍数を取得してみる。
しかし取得はできたものの、リアルタイムの心拍数データではないことに気づく。

  • 一晩試してみたところ、20~40分間隔でデータ更新
  • fitbitアプリで常時接続を有効にすると、15~20分間隔(まれに30分)でデータ更新
  • fitbit社の人によると「Fitbit device will sync every 15–20 minutes.」

心臓が止まった20分後にアラーム音を鳴らしてもあまり意味がないので、別の方法を模索。

ANT+デバイスを使う

サイクルコンピュータで使用されているANT+という2.4GHz帯でデータ送信する仕組みに対応したデバイスであればリアルタイムで心拍数を飛ばせる。これをRaspberry Piで受信する。
以下に掲載のスクリプトはこちらにまとめてあります。

必要なもの

  • Raspberry Pi 3 Model B 本体のほか、マイクロSDカード、電源ケーブルなど
  • Garmin vivosmart HR J
  • Suunto Movestick Mini
  • Raspberry Piにつなぐスピーカー(USBスピーカーMM-SPU8BKを使用 100円ショップのミニスピーカーでは出力が足りず鳴らない)
  • ブレッドボード、ジャンパーワイヤ(メス-オス)6本、タクトスイッチ2個、発光ダイオード1個、抵抗300Ω1個

Raspberry Pi の初期設定

分かっている方は読み飛ばしてください。

データ受信の設定とテスト

Suunto Movestick MiniをUSBポートに刺す

$ dmesg | tail
[49296.697136] usb 1-1.3: new full-speed USB device number 4 using dwc_otg
[49296.839359] usb 1-1.3: New USB device found, idVendor=0fcf, idProduct=1008
[49296.839374] usb 1-1.3: New USB device strings: Mfr=1, Product=2, SerialNumber=3
[49296.839382] usb 1-1.3: Product: Movestick mini
[49296.839390] usb 1-1.3: Manufacturer: Suunto
[49296.839398] usb 1-1.3: SerialNumber: 1618800901

idProductをメモる。ここでは1008。

$ sudo idle3 /etc/udev/rules.d/garmin-ant2.rules

以下を1行で貼り付け。idProductが1008ではない場合は適宜修正。

SUBSYSTEM=="usb", ATTRS{idVendor}=="0fcf", ATTRS{idProduct}=="1008", RUN+="/sbin/modprobe usbserial vendor=0x0fcf product=0x1008", MODE="0666", OWNER="pi", GROUP="root"

一度再起動し、baderjさんがフォークしたpython-antをインストール。

$ sudo pip install git+https://github.com/baderj/python-ant.git

https://www.johannesbader.ch/2014/06/track-your-heartrate-on-raspberry-pi-with-ant/ のコードをちょっと修正して実行。
pythonを初めて触ったので変なところがあったらすみません。

heart_rate_monitor.py
"""
    Code based on:

https://github.com/mvillalba/python-ant/blob/develop/demos/ant.core/03-basicchannel.py

    in the python-ant repository and

https://github.com/tomwardill/developerhealth

    by Tom Wardill and

https://www.johannesbader.ch/2014/06/track-your-heartrate-on-raspberry-pi-with-ant/

    by Johannes Bader
"""
import sys
import time
from ant.core import driver, node, event, message, log
from ant.core.constants import CHANNEL_TYPE_TWOWAY_RECEIVE, TIMEOUT_NEVER

class HeartRateMonitor(event.EventCallback):

    DEFAULT_SERIAL = "/dev/ttyUSB0"
    DEFAULT_NETKEY = 'B9A521FBBD72C345'.decode('hex')

    def __init__(self, call_func, serial = None, netkey = None):
        self.call_func = call_func
        self.serial = serial or HeartRateMonitor.DEFAULT_SERIAL
        self.netkey = netkey or HeartRateMonitor.DEFAULT_NETKEY
        self.antnode = None
        self.channel = None

    def start(self):
        #print("starting node")
        self._start_antnode()
        self._setup_channel()
        self.channel.registerCallback(self)
        #print("start listening for hr events")

    def stop(self):
        if self.channel:
            self.channel.close()
            self.channel.unassign()
        if self.antnode:
            self.antnode.stop()

    def __enter__(self):
        return self

    def __exit__(self, type_, value, traceback):
        self.stop()

    def _start_antnode(self):
        stick = driver.USB2Driver(self.serial)
        self.antnode = node.Node(stick)
        self.antnode.start()

    def _setup_channel(self):
        key = node.NetworkKey('N:ANT+', self.netkey)
        self.antnode.setNetworkKey(0, key)
        self.channel = self.antnode.getFreeChannel()
        self.channel.name = 'C:HRM'
        self.channel.assign('N:ANT+', CHANNEL_TYPE_TWOWAY_RECEIVE)
        self.channel.setID(120, 0, 0)
        self.channel.setSearchTimeout(TIMEOUT_NEVER)
        self.channel.setPeriod(8070)
        self.channel.setFrequency(57)
        self.channel.open()

    def process(self, msg):
        if isinstance(msg, message.ChannelBroadcastDataMessage):
            #print("heart rate is {}".format(ord(msg.payload[-1])))
            heart_rate = int(ord(msg.payload[-1]))
            self.call_func(heart_rate)

def my_print(data):
    print(data)

if __name__ == '__main__':
    hrm = HeartRateMonitor(my_print)
    print("monitor start")
    hrm.start()
    print("monitor start listening")
    print("press Ctrl-C to stop this script")
    while True:
        try:
            time.sleep(1)
        except KeyboardInterrupt:
            print("monitor stop")
            hrm.stop()
            sys.exit(0)

上をコピペしなくても以下からダウンロードできます。

$ wget https://github.com/kswc/heart_rate_alarm/raw/master/heart_rate_monitor.py

Garmin vivosmart HR Jを装着し、画面をスライドして心拍数表示画面に切り替え、画面を2秒ほど長押しし、心拍転送モードに切り替え。
heart_rate_monitor.pyを実行して、以下のように心拍数が表示されれば成功。

$ python heart_rate_monitor.py
monitor start
monitor start listening
press Ctrl-C to stop this script
68
68
68
...

アラート用オーディオファイルの再生テスト

/home/pi/に、心拍数異常時に鳴らすalert.mp3、スクリプト開始時に鳴らすstart.mp3、終了時に鳴らすstop.mp3を保存。
オーディオ端子またはUSB端子にスピーカーを接続。設定できるならbluetooth接続でもOK。
USB接続のMM-SPU8BKでは、オーディオ端子にスピーカーを接続した際の無音時ホワイトノイズは無いものの、音量を最大にすると音割れあり。ただ、大音量で鳴らして気付いてもらうという用途なので支障ない。

オーディオ端子から鳴らす場合はデスクトップ画面右上のボリュームアイコンを右クリックして「Analog」を選択。USB接続のスピーカーであれば該当するものを選択。
audio_select.png

audio_test.py
import pygame.mixer
from time import sleep

pygame.mixer.init()
pygame.mixer.music.load("start.mp3")
pygame.mixer.music.set_volume(1.0)
pygame.mixer.music.play(1)
sleep(1)
pygame.mixer.music.load("alert.mp3")
pygame.mixer.music.set_volume(1.0)
pygame.mixer.music.play(-1)
sleep(10)
pygame.mixer.music.stop()
sleep(1)
pygame.mixer.music.load("stop.mp3")
pygame.mixer.music.set_volume(1.0)
pygame.mixer.music.play(1)

テスト再生してスピーカーから音が出れば成功。

$ wget https://github.com/kswc/heart_rate_alarm/raw/master/audio_test.py
$ python audio_test.py

心拍数取得とオーディオ再生が成功したら、心拍数が指定範囲を超えた際にオーディオが再生されるかをテスト。

heart_rate_alarm.py
import sys
import pygame.mixer
from time import sleep
from heart_rate_monitor import HeartRateMonitor

class HeartRateAlarm:

    def __init__(self, alarm_heart_rate_min=40, alarm_heart_rate_max=180):
        self.alarm_heart_rate_min = alarm_heart_rate_min
        self.alarm_heart_rate_max = alarm_heart_rate_max
        self.alarm_now = False

        self.monitor = HeartRateMonitor(self.check_heart_rate)
        print("monitor start")
        self.monitor.start()
        print("monitor listening")

        pygame.mixer.init()
        pygame.mixer.music.load("start.mp3")
        pygame.mixer.music.set_volume(1.0)
        pygame.mixer.music.play(1)

    def check_heart_rate(self, hr):
        if self.alarm_heart_rate_min <= hr <= self.alarm_heart_rate_max:
            sys.stdout.write("%d " % hr)
            sys.stdout.flush()
            if self.alarm_now:
                sys.stdout.write("alarm off. ")
                self.alarm_now = False
                pygame.mixer.music.stop()
        else:
            sys.stdout.write("%d! " % hr)
            sys.stdout.flush()
            if not self.alarm_now:
                sys.stdout.write("alarm on. ")
                self.alarm_now = True
                pygame.mixer.music.load("alert.mp3")
                pygame.mixer.music.set_volume(1.0)
                pygame.mixer.music.play(-1)

    def stop(self):
        self.alarm_now = False
        print("monitor stop")
        self.monitor.stop()
        pygame.mixer.music.load("stop.mp3")
        pygame.mixer.music.set_volume(1.0)
        pygame.mixer.music.play(1)
        pygame.mixer.music.stop()


if __name__ == '__main__':
    alarm_heart_rate_min = 60
    alarm_heart_rate_max = 80
    alarm = HeartRateAlarm(alarm_heart_rate_min, alarm_heart_rate_max)
    print("press Ctrl-C to stop this script.")

    while True:
        try:
            sleep(1)
        except KeyboardInterrupt:
            alarm.stop()
            sys.exit(0)
$ wget https://github.com/kswc/heart_rate_alarm/raw/master/heart_rate_alarm.py
$ python heart_rate_alarm.py

心拍数の範囲はスクリプト中の

    alarm_heart_rate_min = 60
    alarm_heart_rate_max = 80

で指定しているので、設定を変えてみてちゃんと動くか確認。

起動・終了スイッチ、動作中LEDを使う

以下のとおり配線。
fritzing.PNG

以下のスクリプトで動作テスト。

gpio_test.py
import RPi.GPIO as GPIO
from time import sleep

def event_callback(gpio_pin):
    print("GPIO[%d] callback" % gpio_pin)

GPIO.setmode(GPIO.BCM)
GPIO.setup(21, GPIO.OUT)
GPIO.setup(13, GPIO.IN, pull_up_down = GPIO.PUD_UP)
GPIO.add_event_detect(13, GPIO.RISING, callback=event_callback, bouncetime=2000)

for var in range(0, 20):
    GPIO.output(21, GPIO.HIGH)
    sleep(0.5)
    GPIO.output(21, GPIO.LOW)
    sleep(0.5)

try:
    while True:
        sleep(1)
except KeyboardInterrupt:
    print '\nbreak'
    GPIO.remove_event_detect(13)
    GPIO.cleanup()
$ wget https://github.com/kswc/heart_rate_alarm/raw/master/gpio_test.py
$ python gpio_test.py

LEDが10秒間点滅し、右側のタクトスイッチを押すとコンソールにGPIO[13] callbackと表示されればテストは成功。
心拍数監視の完成版スクリプトは以下のとおり。

heart_rate_alarm_with_gpio.py
import os
import sys
import time
import pygame.mixer
import multiprocessing
import RPi.GPIO as GPIO
from time import sleep
from heart_rate_monitor import HeartRateMonitor

class HeartRateAlarm:

    def __init__(self, alarm_heart_rate_min=40, alarm_heart_rate_max=180):
        self.alarm_heart_rate_min = alarm_heart_rate_min
        self.alarm_heart_rate_max = alarm_heart_rate_max
        self.alarm_now = False

        GPIO.setmode(GPIO.BCM)
        GPIO.setup(21, GPIO.OUT)
        GPIO.setup(13, GPIO.IN, pull_up_down = GPIO.PUD_UP)
        GPIO.add_event_detect(13, GPIO.RISING, callback=self.stop_and_shutdown, bouncetime=200)

        self.led = LEDBlinker()
        self.shared_heart_rate = multiprocessing.Value('i', 0)
        self.shared_heart_rate_update_time = multiprocessing.Value('d', 0)
        self.led_process = multiprocessing.Process(target=self.led.blink_with_bpm, args=(self.shared_heart_rate, self.shared_heart_rate_update_time))
        self.led_process.start()

        self.monitor = HeartRateMonitor(self.check_heart_rate)
        print("monitor start")
        self.monitor.start()
        print("monitor listening")
        self.led.start()

        pygame.mixer.init()
        pygame.mixer.music.load("start.mp3")
        pygame.mixer.music.set_volume(0.1)
        pygame.mixer.music.play(1)

    def check_heart_rate(self, hr):
        if self.alarm_heart_rate_min <= hr <= self.alarm_heart_rate_max:
            sys.stdout.write("%d " % hr)
            sys.stdout.flush()
            if self.alarm_now:
                sys.stdout.write("alarm off. ")
                self.alarm_now = False
                pygame.mixer.music.stop()
        else:
            sys.stdout.write("%d! " % hr)
            sys.stdout.flush()
            if not self.alarm_now:
                sys.stdout.write("alarm on. ")
                self.alarm_now = True
                pygame.mixer.music.load("alert.mp3")
                pygame.mixer.music.set_volume(1.0)
                pygame.mixer.music.play(-1)
        self.shared_heart_rate.value = hr
        self.shared_heart_rate_update_time.value = time.time()

    def stop(self):
        self.alarm_now = False
        print("monitor stop")
        self.monitor.stop()
        pygame.mixer.music.stop()
        pygame.mixer.music.load("stop.mp3")
        pygame.mixer.music.set_volume(0.1)
        pygame.mixer.music.play(1)
        sleep(1)
        self.shared_heart_rate.value = 0
        self.led_process.terminate()
        self.led.stop()
        GPIO.cleanup() 

    def stop_and_shutdown(self, gpio_pin):
        print("GPIO[%d] callback" % gpio_pin)
        if gpio_pin != 13:
            return
        self.stop()
        sleep(1)
        print("sudo shutdown -h now")
        #sys.exit(0)
        os.system("sudo shutdown -h now")


class LEDBlinker:

    def __init__(self):
        pass

    def start(self):
        for var in range(0, 20):
            GPIO.output(21, GPIO.HIGH)
            sleep(0.1)
            GPIO.output(21, GPIO.LOW)
            sleep(0.1)

    def blink_with_bpm(self, bpm, update_time):
        while True:
            temp_bpm = bpm.value
            if update_time.value < time.time() - 3:
                GPIO.output(21, GPIO.HIGH)
                sleep(1)
            elif temp_bpm == 0:
                GPIO.output(21, GPIO.LOW)
                sleep(1)
            else:
                GPIO.output(21, GPIO.HIGH)
                sleep(60.0 / temp_bpm / 2)
                GPIO.output(21, GPIO.LOW)
                sleep(60.0 / temp_bpm / 2)

    def stop(self):
        for var in range(0, 10):
            GPIO.output(21, GPIO.HIGH)
            sleep(0.1)
            GPIO.output(21, GPIO.LOW)
            sleep(0.1)


if __name__ == '__main__':
    alarm_heart_rate_min = 40
    alarm_heart_rate_max = 150
    alarm = HeartRateAlarm(alarm_heart_rate_min, alarm_heart_rate_max)
    print("press Ctrl-C to stop this script.")

    while True:
        try:
            sleep(1)
        except KeyboardInterrupt:
            alarm.stop()
            sys.exit(0)
$ wget https://github.com/kswc/heart_rate_alarm/raw/master/heart_rate_alarm_with_gpio.py
$ python heart_rate_alarm_with_gpio.py

スクリプトを実行すると、心拍数に合わせてLEDが点滅。
led_blink.gif
心拍数が送信されていないときは点灯。心拍数がゼロのとき(測定できないときを含む)は消灯。
右側のタクトスイッチを押すとstop.mp3を鳴らしてシャットダウン。
左側のタクトスイッチでシャットダウンからの起動。

起動時にスクリプトを自動実行する

$ idle3 ~/.config/lxsession/LXDE-pi/autostart 

最終行に以下の記載を追加

@/usr/bin/python /home/pi/heart_rate_alarm_with_gpio.py

autostart.PNG

補足

  • スクリプト実行中にvivosmart HR Jを腕から外すと、心拍数ゼロというデータが送信され、アラーム音が鳴ってしまう。寝る前に左のタクトスイッチでRaspberry Piを起動し、起きたら右のタクトスイッチで終了させる。
  • vivosmart HR Jの場合、心拍数転送モードでは一晩でバッテリーを半分ぐらい消費するため、寝ている間だけ着けて起きたら充電するのが無難。
  • vivosmart HR Jを充電後に装着した際、心拍数が時々しか取得されない状態に一度なった。腕に装着しているにもかかわらず緑色の測定光がすぐ消えてしまうという症状。vivosmart HR Jをリセットすると直った。

がんばらなかったこと

  • python-antはたくさんフォークされていて最近でも https://github.com/mch/python-ant/ で活発に修正されているが、ちょっと試してうまくいかなかったので使わなかった。
  • Raspberry Pi 1ではANT+受信機を刺したUSBコネクタがとても熱くなったのでRaspberry Pi 3に変えた。
  • heart_rate_alarm_with_gpio.pyをCtrl+Cで止めるとスレッドエラーが出る。
  • 起動時にスクリプトを自動実行する設定方法はいくつかあるが、systemctlでserviceとして起動する方法では音が出なかった。

参考にさせていただいたところ