Raspberry Pi Zero2 W でGPSロガーを作ってみる – 第02回 実装・動作確認編

重い腰を上げて自作GPSロガー作成にやっとこ着手したgoldear@goldear820です。

自宅環境で上手くGPSが受信できないRaspberry Pi Zero WH が非力過ぎて無線でSSH接続すると反応が悪いなど、言い訳でしかありません。

最近スマートアクアリウムでRaspberry Piを使用する機会があり、再び自作GPSロガーに挑戦してみようという意欲が湧いてきたので、作っていきます。

初期セットアップは前回記事にまとめましたので、そちらを参考にどうぞ。

Raspberry Pi Zero WH でGPSロガーを作ってみる - 第01回 スタートアップ編
このブログで旅行のお供にGPSロガーアプリをオススメしてきたgoldear@goldear820です。 以前から気にはなっていたものの手を出さずにいた自作GPSロガーだったのですが、仕事でLinuxソフトに触れる機会が多くなり、慣れるという...
スマートアクアリウムのススメ① Raspberry Pi で 水温測定【DS18B20】
北海道で買ってきたまりもをこぢんまりと育てるつもりが、どんどんアクアリウムにハマって後戻りできない状態のgoldear@goldear820です。 アクアリウムで非常に重要になってくるのが「水温管理」ですが、我が家はNASが24時間稼働して...
スポンサーリンク

全体図

目標は私が長年愛用していたM241 GPSロガーにします。このM241はとても優秀だったのですが、2021年のロールーオーバー非対応となり、使おうと思えば使えるのですが、使い勝手が悪くなってしまい使用を断念。。。

要件としては以下の通りです。ただし、可能な限り小型なGPSロガーを目指したいので、今後アップデートを重ねていきたいです。

要件一覧
・モバイルバッテリー(5V1A)で動作
・OLEDにGPS情報表示
 - 年月日-日時
 - 緯度, 経度
 - 移動速度
 - 海抜
 - ログ状態
・OLED ON/OFF切り替えスイッチ
・GPS情報をCSVファイルに記録保存

GPSロガー Holux M-241をサイクリングに使ってみた
前回の千葉 手賀沼サイクリングでGPSロガーを使って位置情報を記録してみました。 意外と簡単にGoogleマップに表示させることができますし、走った道も振り返ることができるので面白いです。今回は私が使用してるGPSロガーやツール、Googl...

準備物

Raspberry Pi Zero2 W

第01回は Zero W を使用しましたが、非力なのでZero2 W に乗り換えました。

スイッチサイエンスで在庫が復活、販売制限も解除されて購入しやすくなっています。

Raspberry Pi Zero 2 W
512 MBのRAMと、1 GHz駆動の64 bit Arm Cortex-A53 クアッドコア BCM2710A1ダイを中心としたRaspberry Pi RP3A0 SiPを搭載した、Raspberry Pi Zeroファミリーの最新製...

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

GPSモジュールは秋月電子で販売されていた物を使用しますが、同モジュールは現在販売終了しています。

使い勝手が落ちてしまいますが、代替のGPSモジュールも販売されているので以下に挙げておきます。

AmazonでもGPSモジュールは取り扱っていますが、仕様が不明なので秋月で取り扱っている物の方が安心できます。

GPS-54型GPSレシーバーモジュール: 通信・無線モジュール・アンテナ 秋月電子通商-電子部品・ネット通販
電子部品,通販,販売,半導体,IC,LED,マイコン,電子工作GPS-54型GPSレシーバーモジュール秋月電子通商 電子部品通信販売
GPS受信機 シリアル出力タイプ(先バラ) みちびき2機(194/195)対応 1PPS出力付 GT-502MGG-N: 通信・無線モジュール・アンテナ 秋月電子通商-電子部品・ネット通販
電子部品,通販,販売,半導体,IC,LED,マイコン,電子工作GPS受信機 シリアル出力タイプ(先バラ) みちびき2機(194/195)対応 1PPS出力付 GT-502MGG-N秋月電子通商 電子部品通信販売

OLED SSD1306

GPSモジュールで取得した情報を表示するI2C接続のOLED SSD1306(128×64)です。

0.96インチ 128×64ドット有機ELディスプレイ(OLED) 白色: オプトエレクトロニクス 秋月電子通商-電子部品・ネット通販
電子部品,通販,販売,半導体,IC,LED,マイコン,電子工作0.96インチ 128×64ドット有機ELディスプレイ(OLED) 白色秋月電子通商 電子部品通信販売

初めはMSP2807というSPI接続 2.8inch 320×240 TFT液晶で実装しようと考えていたのですが、1秒毎の更新だと描画が間に合わないので使用を断念しました。

ソフトの書き方で解決できそうですが、そこまでのスキルは持ち合わせていないので、大人しく表示解像度を下げました。

オルタネイト動作スイッチ

GPS情報をCSVに書き込む or 書き込まない を切り替えるスイッチと、OLED表示のON/OFFを切り替えるスイッチの2つを実装します。

とりあえずブレッドボード二実装するのでスライドスイッチを使用しますが、ケース実装時はトグルスイッチを使用しようと考えています。

GPSセンテンスについて

GPSモジュールから取得するデータはNMEA0183というフォーマットで定められている各種センテンスにまとめられています。

NMEA0183フォーマットを解析・抽出するツールはあるのですが、今回はそれらを使用せずにセンテンスを解析・抽出していきます。

センテンスについては、以下の製品仕様所やページにまとめられていて分かりやすいです。

GPS-54型GPSレシーバーモジュール: PDF仕様書

技術情報 – その他情報 | 【ALES】位置補正情報生成/ 配信サービス
衛星から受信したGNSS信号を基に、 補正情報を生成/配信し、 誤差数cmの高精度な測位を実現
NMEAフォーマット

設定

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フォーマットに変換する記事をまとめました。

Raspberry Pi Zero2 W でGPSロガーを作ってみる – 第03回 CSV to GPX 変換編
最近RaspberryPi沼にハマりかけているgoldear@goldear820です。 前回、ようやくGPSロガーとして形になりましたが、記録したCSVファイルではGoogleMapにインポートすることができません。 GPS情報をわざわざ...

コメント