PicoのGPSロガーに心拍センサを追加

# はじめに
ヤビツ峠など登山口へ向かう道中、サイクリストの方と多くすれ違いますが、たまに自転車に心拍センサを取り付けている人を見かけます。Youtube動画でもGPS情報と心拍数を可視化されているサイクリスト動画があり、「なんかカッコいいなぁ」と思ってました。

そこで、以前作成したPicoのGPSロガーに心拍センサを付けて、疲労度のようなものを可視化しようと考えました。

# 前提
以前作成したPicoのGPSロガーは以下になります。

https://qiita.com/Y-Shikase/items/174f6595e5c16cd26f8b

# 用意したもの
心拍センサーはいつものスイッチサイエンスさんで購入しました。

https://www.switch-science.com/products/2526?variant=42381928988870

心拍数に応じた音を出したいため、圧電ブザーをAmazonで購入しました。
Picoで使うには、3Vから対応しているものが良さそうです。

『オーディオファン 電子ブザー 80dB 電子工作 部品 ネジ止め タイプ DC3-24V ブラック』

https://www.amazon.co.jp/gp/product/B07V3JZ2YS

# 配線とケース収納
GPSが使用しているピンもあるので、心拍センサーとブザーを配線しました。

![配線.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/410957/0ac95d13-556c-3383-fc4a-5e43df7068d7.png)

いつも悩ましいケース問題ですが、大きさ的に93x68x14mmのトレカケースが丁度よい感じでした。
もう少し厚みがある方がいいかもしれません。このケースはダイソーさんに売ってました。

![case.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/410957/236b4231-6b25-43c5-8f8b-77a0b5a00292.png)

パーツを両面テープで固定して、ケーブル通すための穴は空けます。一回り大きく空けて最終的には手芸用のグルーガンで埋めれば良いかと思います。

![PXL_20240107_123207735~2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/410957/066ee893-698f-13a1-27e2-a691882e28af.jpeg)

なかなか良い感じ。

# エンハンスしたプログラム (main.py)
製造元WikiにはPico(Micro Python)用のサンプルプログラムがありません。

https://wiki.seeedstudio.com/Grove-Ear-clip_Heart_Rate_Sensor/

ですが、今はChatGPTがあるのでサンプルプログラムを教えてもらったり、間違いを指摘しつつ修正したりして以下のようなプログラムを作る事ができます。


```python:main.py
from pico import l76x
from pico.micropyGPS.micropyGPS import MicropyGPS
from machine import Pin, PWM
import time

# GPSの初期設定
gnss_l76b=l76x.L76X(uartx=0,_baudrate = 9600)
gnss_l76b.l76x_exit_backup_mode()
gnss_l76b.l76x_send_command(gnss_l76b.SET_SYNC_PPS_NMEA_ON)
parser = MicropyGPS(location_formatting='dd')
sec = 0
tpfile = open("gpslog.csv","a")

# 心拍センサー（デジタル出力）とブザーの設定
hrm_pin = Pin(26, Pin.IN, Pin.PULL_DOWN)
buzzer = PWM(Pin(14))
heart_rates = []
last_heartbeat_time = 0

def beep(slp):
    buzzer.freq(3000)  # ブザーの周波数を設定
    buzzer.duty_u16(32768)  # ブザーをオン（半分のデューティサイクル）
    time.sleep(slp)  # ブザーを鳴らす時間
    buzzer.duty_u16(0)  # ブザーをオフ

def filter_heart_rates(heart_rate, window_size=2):
    if len(heart_rates) < window_size:
        return True

    avg = sum(heart_rates[-window_size:]) / window_size
    return abs(heart_rate - avg) < 30  # 許容する最大の逸脱値

def on_heartbeat(pin):
    global last_heartbeat_time, heart_rates
    current_time = time.ticks_ms()
    if last_heartbeat_time > 0:
        interval = time.ticks_diff(current_time, last_heartbeat_time)
        heart_rate = 60000 // interval
        if filter_heart_rates(heart_rate):
            heart_rates.append(heart_rate)
            print("Heart Rate:", heart_rate)
            # 心拍数に基づいてビープ音の長さを変更
            if heart_rate > 150:
                beep(0.3)
            elif heart_rate > 120:
                beep(0.1)
        else :
            print("Heart Rate [spike!]:", heart_rate)
    last_heartbeat_time = current_time

# 心拍センサーの割り込み設定
hrm_pin.irq(trigger=Pin.IRQ_RISING, handler=on_heartbeat)

# メインループ
while True:
    if gnss_l76b.uart_any():
        try:
            sentence = parser.update(chr(gnss_l76b.uart_receive_byte()[0]))
        except:
            pass
        if sentence and parser.satellites_in_use > 0 and sec != parser.timestamp[2] and (parser.timestamp[2] % 5) == 0:
            datetime = f"20{parser.date[2]:02}-{parser.date[1]:02}-{parser.date[0]:02}T{parser.timestamp[0]:02}:{parser.timestamp[1]:02}:{parser.timestamp[2]:02}Z"
            lat = f"{parser.latitude[0]:.9f}"
            lon = f"{parser.longitude[0]:.9f}"
            alt = int(parser.altitude)
            avg_heart_rate = sum(heart_rates) // len(heart_rates) if heart_rates else 0
            data = f"{datetime},{lat},{lon},{alt},{parser.geoid_height},{parser.fix_stat},{parser.hdop},{parser.satellites_in_use},{avg_heart_rate}\n"
            print(data)
            sec = parser.timestamp[2]
            tpfile.write(data)
            tpfile.flush()
            heart_rates.clear()  # 心拍数リストをクリア
```

当初、フィルタfilter_heart_rates()を導入せずにデータ収集してましたが、晴れた昼間に屋外で使うとかなり精度が悪くなったので、それとなくChatGPTに相談すると、以下のような返事がきました。

https://x.com/ShikaseY/status/1744211792673505689?s=20

少しでも異常値を除去するために、フィルタ関数 filter_heart_rates() を実装しました。
filter_heart_rates() の window_size と return文の数値 で調整してください。
うまく調整すると、実行時にフィルタで異常値を弾いてくれます。

```
>>> %Run -c $EDITOR_CONTENT
$PMTK255,1*2D
Heart Rate: 61
Heart Rate: 55
Heart Rate [spike!]: 327
Heart Rate [spike!]: 309
Heart Rate [spike!]: 29
Heart Rate [spike!]: 118
Heart Rate: 63
Heart Rate: 77
Heart Rate: 73
Heart Rate: 76
Heart Rate: 75
Heart Rate: 72
Heart Rate: 78
 :
```

フィルタ適用前後でcsvファイルに書かれたデータを比較しましたが、少しは効果がありそうです。

https://x.com/ShikaseY/status/1744294570450677865?s=20

これ以上の精度調整は、やはりセンサー周辺を覆う必要がありそうですね。

# Kibanaで可視化

Kibanaのヒートマップで心拍数を可視化しようとしましたが、平均ではなく合計値の集計しかできないようです。これではデータサンプルが集中するような行動(休憩中や同じ道の往来)が濃くなるので純粋な心拍数の可視化ができません。(これは想定外でした...)

![gps+hr.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/410957/24e0bfc5-2a13-a6b6-e70c-acd214dbb57f.png)

とはいえ梅沢登り口の急坂(地図の下部)での心拍数の多さはなんとか可視化できているようです。他に色が濃い部分は展望台、駅前の自販機とサンプル数が多い場所になってます。
なんとか合計ではなく平均で出せないものだろうか・・・

# おわりに

手作りガーミソへの道はまだまだ険しそうですね。(でも楽しい！)