概要
心拍数を送信できる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 の初期設定
分かっている方は読み飛ばしてください。
- Raspbian Jessie with PIXELを書き込んで起動
- 無線LANの設定 http://www.circuitbasics.com/how-to-set-up-wifi-on-the-raspberry-pi-3/ の「CONFIGURE WIFI FROM THE DESKTOP」
- VNCの設定 http://www.denshi.club/pc/raspi/raspberry-pi-windowsvnc.html
- 日本語フォントを追加 http://qiita.com/RyosukeKamei/items/5ecf2aa5d5cda848fe51
- ロケール、タイムゾーン、キーボードの設定 http://ko-log.net/archives/12259655.html
- VNCの解像度を上げる http://e-tipsmemo.hatenablog.com/entry/2016/11/22/004504
- Sambaの導入 http://python.slightlysimple.net/entry/2016/03/13/232611
- アップデートをまとめて実施
sudo apt-get update && sudo apt-get dist-upgrade -y && sudo SKIP_WARNING=1 rpi-update
データ受信の設定とテスト
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を初めて触ったので変なところがあったらすみません。
"""
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接続のスピーカーであれば該当するものを選択。
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
心拍数取得とオーディオ再生が成功したら、心拍数が指定範囲を超えた際にオーディオが再生されるかをテスト。
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を使う
以下のスクリプトで動作テスト。
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
と表示されればテストは成功。
心拍数監視の完成版スクリプトは以下のとおり。
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が点滅。
心拍数が送信されていないときは点灯。心拍数がゼロのとき(測定できないときを含む)は消灯。
右側のタクトスイッチを押すとstop.mp3を鳴らしてシャットダウン。
左側のタクトスイッチでシャットダウンからの起動。
起動時にスクリプトを自動実行する
$ idle3 ~/.config/lxsession/LXDE-pi/autostart
最終行に以下の記載を追加
@/usr/bin/python /home/pi/heart_rate_alarm_with_gpio.py
補足
- スクリプト実行中に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として起動する方法では音が出なかった。
参考にさせていただいたところ
- https://www.johannesbader.ch/2014/06/track-your-heartrate-on-raspberry-pi-with-ant/ Raspberry PiでANT+デバイスの心拍数データを得る手順が載っていて、この記事が無ければ多分できなかった。
- http://www.tispycat.com/vivosmart-hrj-heartratemode-simple/ vivosmart HR Jで心拍転送モードに簡単に切り替える方法が載っている。
- http://hammmm.hatenablog.com/entry/2016/11/14/231337 Raspberry Piのシャットダウンからの起動にはGPIO3を使えばよいことが載っている。
- http://hendigi.karaage.xyz/2016/11/auto-boot/ Raspberry Piでプログラムを自動起動させる手法が載っている。