PicoのGPSロガーにOLEDディスプレイを追加

# はじめに
Picoで自作しているGPSロガーですが、動作状況がわからないので、心拍数やGPS情報を低消費電力のOLEDディスプレイで表示しました。

# 前提

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

https://qiita.com/Y-Shikase/items/c2278fed732d8a071603

の続きでございます。

# 配線とケース収納
よくワンコイン前後で売られている「SSD1306」のOLEDディスプレイを接続します。私の場合、GPSでUART0(GP0,GP1)を使っていたので、I2C0が使えずI2C1(GP18,GP19)に接続しています。

![864372e706e67.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/410957/1d3dc793-e554-d000-cfae-cef8786aa397.png)

![GDuGRkDa4AAFYdT.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/410957/8db51a50-f562-5b8d-9f7d-29468391cebd.jpeg)

# パッケージ追加とプログラム(main.py)
PicoをPCに接続して、ThonnyからOLEDディスプレイ用のパッケージをインストールします。
メニューバーから[ツール]-[パッケージを管理...]をクリックします。

![pkg_menu.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/410957/a5f24a65-3215-fcdd-16de-913f5a502f51.png)

「ssd1306」で検索して、一番上の「micropython-ssd1306」です。
![pkg.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/410957/b438f997-393e-f8ed-91ec-5f3f30ffa304.png)

CSVファイルにログを書き込むタイミングでOLEDの表示も更新するようにしました。表示する項目は以下です。

- 心拍数
- 時間(GPS)
- 経度緯度(GPS)
- 高度(GPS)

表示の更新は、衛星を掴んでから5秒間隔です。

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

# OLEDの初期設定
sda = machine.Pin(18)
scl = machine.Pin(19)
i2c = machine.I2C(1,sda=sda, scl=scl, freq=400000)
oled = ssd1306.SSD1306_I2C(128, 64, i2c)

# 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 add_hours_to_time(hh, mm, ss, add_hours):
    hh += add_hours
    if hh >= 24:
        hh -= 24
    return hh, mm, ss

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=3):
    if len(heart_rates) < window_size:
        return True
    avg = sum(heart_rates[-window_size:]) / window_size
    return abs(heart_rate - avg) < 20  # 許容する最大の逸脱値

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 50 <= heart_rate <= 200: # 心拍数50～200が有効データ範囲
            if filter_heart_rates(heart_rate):
                heart_rates.append(heart_rate)
                print("Heart Rate:", heart_rate)
                # 心拍数に基づいてビープ音の長さを変更
                if heart_rate > 160:
                    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)
prev_hr = 0

# メインループ
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
            if avg_heart_rate == 0:
                avg_heart_rate = prev_hr # 心拍数を計測していない場合は前回値を出力
            else:
                prev_hr = avg_heart_rate
            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()  # 心拍数リストをクリア
            hour, minute, second = add_hours_to_time(parser.timestamp[0], parser.timestamp[1], parser.timestamp[2], 9)
            oled.fill(0)
            oled.rect(0, 0, 127, 63, 1)
            oled.text(f"H:{avg_heart_rate:3}  {hour:02}:{minute:02}:{second:02}",2,5)
            oled.text(f"Geopoint:",2,20)
            oled.text(f" {parser.latitude[0]:.3f},{parser.longitude[0]:.3f}",0,30)
            oled.text(f"Altitude:",2,43)
            oled.text(f" {alt:4}m",0,53)
            oled.show()
```

# 使ってみた
いつもトレーニングで登っている吾妻山公園へ。
二宮、小田原、真鶴まで途切れて続く街灯りが好きです。

![PXL_20240121_094444062.NIGHT.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/410957/f763fa7d-0fb8-37a1-40c7-8b0baca7271e.jpeg)

OLEDは、ばっちり綺麗に表示しています。
何気に時間と高度が分かるのは嬉しいかも。

![PXL_20240121_094312724.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/410957/4f5a72f6-bfd6-d333-1d8e-08648dca5034.jpeg)

# おわりに
まだ本体の制作費に10K円もかかってないと思います(バッテリーは別です)。ディスプレイをもっと大きくしてサイクルコンピュータ？みたいなのも作れそうな気がしますね。

普通なら「ガーミン買って終わり」で既製品もカッコいいのですが、データを自由に流用できる自作デバイスも良いですね！安く作れると満足感もあります。

Pico Wがどこまでできるか調べてませんが、スマホアプリにデータ転送ができるように作れるならPico Wで作ってみたいですね。