
重い腰を上げて自作GPSロガー作成にやっとこ着手したgoldear@goldear820です。
自宅環境で上手くGPSが受信できない、Raspberry Pi Zero WH が非力過ぎて無線でSSH接続すると反応が悪いなど、言い訳でしかありません。
最近スマートアクアリウムでRaspberry Piを使用する機会があり、再び自作GPSロガーに挑戦してみようという意欲が湧いてきたので、作っていきます。
初期セットアップは前回記事にまとめましたので、そちらを参考にどうぞ。


目次
全体図

目標は私が長年愛用していたM241 GPSロガーにします。このM241はとても優秀だったのですが、2021年のロールーオーバー非対応となり、使おうと思えば使えるのですが、使い勝手が悪くなってしまい使用を断念。。。
要件としては以下の通りです。ただし、可能な限り小型なGPSロガーを目指したいので、今後アップデートを重ねていきたいです。
要件一覧
・モバイルバッテリー(5V1A)で動作
・OLEDにGPS情報表示
- 年月日-日時
- 緯度, 経度
- 移動速度
- 海抜
- ログ状態
・OLED ON/OFF切り替えスイッチ
・GPS情報をCSVファイルに記録保存

準備物
Raspberry Pi Zero2 W

第01回は Zero W を使用しましたが、非力なのでZero2 W に乗り換えました。
スイッチサイエンスで在庫が復活、販売制限も解除されて購入しやすくなっています。

アンテナ一体型GPSモジュール

GPSモジュールは秋月電子で販売されていた物を使用しますが、同モジュールは現在販売終了しています。
使い勝手が落ちてしまいますが、代替のGPSモジュールも販売されているので以下に挙げておきます。
AmazonでもGPSモジュールは取り扱っていますが、仕様が不明なので秋月で取り扱っている物の方が安心できます。
OLED SSD1306

GPSモジュールで取得した情報を表示するI2C接続のOLED SSD1306(128×64)です。
初めはMSP2807というSPI接続 2.8inch 320×240 TFT液晶で実装しようと考えていたのですが、1秒毎の更新だと描画が間に合わないので使用を断念しました。
ソフトの書き方で解決できそうですが、そこまでのスキルは持ち合わせていないので、大人しく表示解像度を下げました。
オルタネイト動作スイッチ

GPS情報をCSVに書き込む or 書き込まない を切り替えるスイッチと、OLED表示のON/OFFを切り替えるスイッチの2つを実装します。
とりあえずブレッドボード二実装するのでスライドスイッチを使用しますが、ケース実装時はトグルスイッチを使用しようと考えています。
GPSセンテンスについて
GPSモジュールから取得するデータはNMEA0183というフォーマットで定められている各種センテンスにまとめられています。
NMEA0183フォーマットを解析・抽出するツールはあるのですが、今回はそれらを使用せずにセンテンスを解析・抽出していきます。
センテンスについては、以下の製品仕様所やページにまとめられていて分かりやすいです。
GPS-54型GPSレシーバーモジュール: PDF仕様書

設定
GPSモジュールはシリアルで、OLEDモジュールはI2Cでラズパイと通信するため、あらかじめらSerialとI2C接続を有効化しておく必要があります。
pi@raspberry:~ $ sudo raspi-config
上記コマンドを実行することで、設定画面が表示されます。

「3 Interface Options」を選択します。
※ Debian 12ベースのRaspberry Pi OS「Bookworm」の設定画面です。バージョン違いで設定項目が異なる可能性があります。

「I5 I2C」, 「I6 Serial Port」を選択します。

I2C設定: 「はい」を選択すると、I2Cが有効化されます。

シリアル設定1: ログインには使用しないので「No」を選択

シリアル設定2: GPSモジュールのために使用するので「Yes」を選択
以上を実行することでラズパイのI2Cとシリアル通信が有効化されます。
実装
プログラムはpythonで実装、GPS情報の解析・抽出・CSV書込みするプログラムと、CSV読込してOLEDに表示するプログラムの2つを用意します。
あらかじめ pytz, dateutil, MicropyGPS モジュールを インストールしておいてください。
追記2024-01-17: MicropyGPSは使用せずに自力で解析しているのでインストール不要です。ソースコードにも含まれますが、使用していません。
ソフトウェア記述は不慣れなので、間違った記述・より適切な記述がありましたら、適宜修正をお願いします。
#!/usr/bin/env python3
# encoding: utf-8
# common
import sys
import re
import os
import RPi.GPIO as GPIO
import pytz
from dateutil import *
# GPS
import serial
from micropyGPS import MicropyGPS
# Constant
## Set GPIO Pin
SW_GPIO_0 = 22
## GPS Config
TIME_ZONE = 9 # (UTF+TIME_ZONE->JST)
OUTPUT_PATH = "/home/goldear/gps_logger/"
# MAIN FUNCTION
def get_sentence():
# Config GPIO
GPIO.setmode(GPIO.BCM)
GPIO.setup(SW_GPIO_0, GPIO.IN, pull_up_down=GPIO.PUD_DOWN)
# Create Instance
ser = serial.Serial('/dev/ttyS0', 9600, timeout=10)
gps = MicropyGPS(TIME_ZONE, 'dd')
# Init valiable
get_flg = 0
l_gpx =[]
while True:
try:
sentence = ser.readline().decode('utf-8')
if sentence[0] != '$':
print('Not Matched $')
continue
gps_segments = re.split("[,*]",sentence)
except UnicodeDecodeError:
print('Decode Error')
continue
except KeyboardInterrupt:
GPIO.cleanup()
sys.exit()
## Valid in $GPRMC
if gps_segments[0] == "$GPRMC":
val = (str(gps_segments[2]))
if val == "V":
get_flg = 0
print("NOT READY!")
continue
else: # "A"
get_flg = 1
dd = str(gps_segments[9][0:2])
mm = str(gps_segments[9][2:4])
yy = str(gps_segments[9][4:6])
utc_date= str("20"+yy+"-"+mm+"-"+dd)
## Speed in $GPVTG
if gps_segments[0] == "$GPVTG" and get_flg == 1:
speed = str(gps_segments[7])
## Date in $GPGGA
if gps_segments[0] == "$GPGGA" and get_flg == 1:
# Calc JST
utc_datetime = utc_date + " " + gps_segments[1][0:2] + ":" \
+ gps_segments[1][2:4] + ":" \
+ gps_segments[1][4:6] \
+ "+00:00"
jst_datetime = str(parser.parse(utc_datetime).astimezone(pytz.timezone('Asia/Tokyo')))
l_gpx.append( jst_datetime[0:10]+" "+jst_datetime[11:19] )
# Calc latitude
l_gpx.append (str(gps_segments[3]))
l_gpx.append(str(gps_segments[2][0:2]) + "." + str(gps_segments[2][2:4]) \
+ str(gps_segments[2][5:]))
# Calc longitude
l_gpx.append(str(gps_segments[5]))
l_gpx.append(str(gps_segments[4][0:3]) + "." + str(gps_segments[4][3:5]) \
+ str(gps_segments[4][6:]))
# Calc elevation
l_gpx.append(str(gps_segments[9]))
# Add speed
l_gpx.append(str(speed))
# Add UTC
l_gpx.append(utc_date + "T" + gps_segments[1][0:2] + ":" \
+ gps_segments[1][2:4] + ":" \
+ gps_segments[1][4:] + "Z")
# Create CSV LOG
## Check Exist file
file_name = str(jst_datetime[0:10] + ".csv")
is_file = os.path.isfile(OUTPUT_PATH + file_name)
if is_file:
pass
else:
with open(OUTPUT_PATH + file_name, mode="x", encoding="utf-8"):
print("Create file: " + file_name)
print(l_gpx)
## Write log
if GPIO.input(SW_GPIO_0):
with open(OUTPUT_PATH + file_name, mode="a", encoding="utf-8") as f:
s_gpx = ','.join([str(x) for x in l_gpx])
f.write(s_gpx + "\n")
"""
s_gpx format
[0]: JST "20yy-mm-dd HHMMSS"
[1]: "N"orthern or "S"outhern
[2]: Latitude dd.mmmmmmm
[3]: "E"ast or "W"est
[4]: Longitude ddd.mmmmmm
[5]: Elevation [m]
[6]: Speed [km/h]
[7]: UTC "20yy-mm-ddTHHMMSSZ"
"""
else:
pass
# Clear List
l_gpx = []
if __name__ == "__main__":
get_sentence()
#!/usr/bin/env python3
# encoding: utf-8
# common
import time
import os
import sys
import datetime
import RPi.GPIO as GPIO
# Disp
import board
import busio
import adafruit_ssd1306
from PIL import Image
from PIL import ImageDraw
from PIL import ImageFont
from time import sleep
# Constant
## Config SSD1306-I2C
DEV_ADDR = 0x3C
DISP_W = 128
DISP_H = 64
## Set GPIO Pin
SW_GPIO_0 = 22
SW_GPIO_1 = 27
## GPS Config
LOG_PATH = "/home/goldear/gps_logger/"
####################################
# SUB FUNCTION
def init_display():
# Create Instance SPI & ILI9341
i2c = busio.I2C(board.SCL, board.SDA)
disp = adafruit_ssd1306.SSD1306_I2C(DISP_W, DISP_H, i2c, addr=DEV_ADDR)
return disp
def set_text(draw, str, font, pos):
if pos == "center":
draw.text((DISP_W/2, DISP_H/2), str, font=font, fill=255, anchor="mm")
else :
draw.text((0, pos*12), str, font=font, fill=255)
return
def clear_display(disp):
disp.fill(0)
disp.show()
return
def create_blank_image(disp_width, disp_height):
image = Image.new("1", (disp_width, disp_height))
draw = ImageDraw.Draw(image)
return image, draw
def disp_image(disp, image):
disp.image(image)
disp.show()
return
####################################
# MAIN FUNCTION
def disp_info():
# Init valiable
l_gpx =[]
l_gpx_pre = []
cnt = 0
# Config GPIO
GPIO.setmode(GPIO.BCM)
GPIO.setup(SW_GPIO_0, GPIO.IN, pull_up_down=GPIO.PUD_DOWN)
GPIO.setup(SW_GPIO_1, GPIO.IN, pull_up_down=GPIO.PUD_DOWN)
# Init Display
oled = init_display()
# Clear disp
clear_display(oled)
# Load a font
font0 = ImageFont.truetype("/usr/share/fonts/truetype/fonts-japanese-gothic.ttf", 24)
font1 = ImageFont.truetype("/usr/share/fonts/truetype/fonts-japanese-gothic.ttf", 12)
while True:
try:
# Back Light off
if GPIO.input(SW_GPIO_1) :
oled.poweron()
else:
oled.poweroff()
# Create blank image
image, draw = create_blank_image(oled.width, oled.height)
# Get date
dt_now = datetime.datetime.now()
## Check Exist file
file_name = (dt_now.strftime("%Y-%m-%d") + ".csv")
is_file = os.path.isfile(LOG_PATH + file_name)
if is_file:
with open(LOG_PATH + file_name, mode="r", encoding="utf-8") as f:
last_line = f.readlines()[-1]
l_gpx = last_line.split(",")
else:
set_text(draw, "NOT READY!", font0, "center")
disp_image(oled, image)
sleep(1)
continue
# Check Capture GPS
if l_gpx_pre == l_gpx:
cnt = cnt + 1
else:
cnt = 0
# Clear List
l_gpx_pre = l_gpx
if cnt >= 15:
set_text(draw, "NOT READY!", font0, "center")
else:
# Set info
set_text(draw, l_gpx[0], font1, 0)
set_text(draw, l_gpx[1] + ": " + l_gpx[2], font1, 1)
set_text(draw, l_gpx[3] + ": " + l_gpx[4], font1, 2)
set_text(draw, "speed[km/h]: " + l_gpx[6], font1, 3)
if GPIO.input(SW_GPIO_0) :
set_text(draw, "ele[m]: " + l_gpx[5] + " GET", font1, 4)
else:
set_text(draw, "ele[m]: " + l_gpx[5] + " WAIT", font1, 4)
# Display image
disp_image(oled, image)
# Clear List
l_gpx = []
# Wait...
sleep(1)
except KeyboardInterrupt:
oled.poweroff()
GPIO.cleanup()
sys.exit()
if __name__ == "__main__":
disp_info()
動作確認

2つのプログラムを実行後、OLEDにGPS情報が表示されることを確認できました。
OLEDのON/OFFスイッチ、CSV書込み制御スイッチの動作も確認できました。
(書込みスイッチがOFFだと、OLED表示はWAITが表示されます)
まとめ
GPSのNMEA0183フォーマットの理解必要ですが、なんとか形にすることができました。
ただし、今回生成したCSVでは汎用性のないデータであり、Google MAPにインポートができません。
次回は独自で作成したCSVファイルを汎用的なGPXファイルに変換するところを記事にする予定です。
追記2024-01-16: 独自CSVフォーマットをGPXフォーマットに変換する記事をまとめました。



コメント