重い腰を上げて自作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フォーマットに変換する記事をまとめました。
コメント