Raspberry Pi Zero2 でGPSロガーを作ってきたgoldear@goldear820です。
Raspberry Pi Zero2では小型化に限界がある、OSが不安定になることがあるため、小型かつOSレスのWaveShare製 Raspberry Pi Pico互換ボード RP2040-Tinyを使って、GPSロガーを作ってみました。
試作機を4台ほど作って、ついに5台目でそこそこのものができたのでまとめておきます。
目次
Raspberry Pi RP2040 について
RaspberryPi財団が開発したマイコンチップです。Raspberry Piの名を掲げているだけあって、Pythonを使って(正確にはMicroPython)ソフトを実装することが可能です。
もうPICマイコン使ってGPSロガー作っちゃえばいいじゃんとか思いましたが、ここまでPythonで実装してきたので最後までPythonで実装していきます。
Zero2ではなくRP2040をGPSロガーに使用するメリットとしては
1. OSレスなので電源投入後からGPS信号を受信処理できる
2. 低消費電力なので、長時間駆動が可能
3. (互換ボードだけだが、) GPIOポート数を減らして小型化されている
4. 安くて入手性が良い
が挙げられます。
逆にRP2040のデメリットとしては
1. MicroPythonはPython(いわゆるCPython)と同じコード記述はできない
2. フラッシュメモリ容量を考慮したコード記述が必要
が挙げられます。
私がソフトウェア実装に不慣れということもありますが、結構苦しめられました。
回路図
以前まではパワーポイントを使って描いていましたが、今回はKiCadの回路エディタを使ってみました。
モジュールの組み合わせなので、特に面白みのない回路になっています。
補足として、USB電源から電流の逆流を防ぐため、DCDCコンバータ(U2)の後段にショットキーバリアダイオード(D2)を追加しています。そのためRP2040-Tiny(U1)の5Vポートに入力されるのは約4.7Vになります。
RP2040-Tiny(U1)内部のLDO(RT9013-33)で3.3Vを出力するためには最低でも3.8Vの入力が必要なので、今回は問題にはなりません。また周辺モジュールも3.3V駆動のため、5Vは必要としません。
ダイオードがない状態で、USB接続した時に電流が逆流してケーブルが溶けてしまったことがあります。。。保護回路って大事だなって感じました。。。
部品表
図番 | メーカー | 品名 | 購入先 |
---|---|---|---|
U1 | WaveShare | RP2040-Tiny | SwitchScience |
U2 | Strawberry Linux | TPS61230A 昇圧型DC-DCコンバータモジュール(5V 2.5A) | Strawberry Linux |
U3 | ACEIRMC | Micro SD Card Adapter (SPI I/F) | Amazon |
U4 | YIC | GPS module GT-502MGG-N | 秋月電子通商 |
U5 | VKLSVAN | 0.96inch OLED SSD1306 | Amazon |
SW1 | ミヤマ電気 | トグルスイッチ MS-600K-B (3A) | 秋月電子通商 |
SW2,SW3 | XiaMen JinBeili | スライドスイッチ (0.1A) | 秋月電子通商 |
C1 | 村田製作所 | 積層セラミックコンデンサ 470pF 50V | 秋月電子通商 |
D1 | OptoSupply | 抵抗内蔵 3mm LED | 秋月電子通商 |
D2 | PANJIT INTERNATIONAL | ショットキーバリアダイオード 45V2A | 秋月電子通商 |
BT1 | NLAセレクト | 18650型 リチウム電池 | 直販サイト |
無 | BENECREAT | 収納ケース | Amazon |
補足として、18650型リチウム電池(BT1)は過電流・過充電・過放電などから保護する回路を内蔵している電池、PSEを取得している電池、を使用してください。保護回路の内蔵されていない、いわゆる「生セル」は保護回路を外付けして使用するもので、今回の回路には適しません。
安いからと言ってAmazonで使えそうなものを購入したが、MicroSDカードを抜き差ししていたらコネクタがもげました・・・(袋のパッケージから取り出したら、すでにもげたものもあった)
抜き挿しのしやすさや、部品の頑丈さを考えたら秋月電子で販売しているヒロセのMicroSDカードDIP化基板をオススメします。なかなか入荷しないんですよねこれ。
上記の安いmicroSDカードスロットを使用する場合は、以前紹介したエポキシ樹脂で固定するモゲマイクロ対策を施すのが有効です。
実装
開発環境はthonnyを使って、MicroPythonで実装します。事前にthonnyの「パッケージ管理」から os, datetime, sdcard, ssd1306 をインストールしておいてください。
また micropyGPS という便利なライブラリもMicroPython対応しているので使用可能ですが、私自身が動作を理解しきれていないため今回は使用しません。使用したら記述量が一気に少なくなります。
RP2040互換ボードであれば、# Config 箇所を修正することで他のボードや、他のポートを使用できます。
注意点として、SPIフラッシュにRaspberry Pi Pico用のUF2ファイルを書き込んでいるため、UARTのポート設定をする箇所はRaspberry Pi Pico のピンアサインで設定します。
########################### # MicroPython # for WaveShare RP2040-Tiny # GPS: GT-502MGG-N # UF2: RPI_PICO-20240105-v1.22.1.uf2 # Author: goldear.net # Date: 2024/02/24 # Rev: 02 ############################ #################################################### # import Library ## common import os import time import datetime import gc from machine import Pin from machine import UART, I2C, SPI ## GPS ## OLED SSD1306 from ssd1306 import SSD1306_I2C ## SDCard import sdcard #################################################### # Config ## GPS UART_ID = 1 UART_RX = 7 # RPi Pico Pin Number 7 <-> GP5 UART_TX = 6 # RPi Pico Pin Number 6 <-> GP4 BAUDRATE = 9600 TIME_ZONE = 9 # (UTC+TIME_ZONE->JST) ## OLED SSD1306 I2C_ID = 0 I2C_SDA = 0 # GP0 I2C_SCL = 1 # GP1 I2C_FREQ = 400000 DISP_W = 128 DISP_H = 64 OUTPUT_PATH = "/sd/" ## Panel Switch SW0_GPIO = 2 # GP2 SW1_GPIO = 3 # GP3 ## SDCard SPI_ID = 1 SPI_RX = 12 # GP12 SPI_CS = 13 # GP13 SPI_SCK = 14 # GP14 SPI_TX = 15 # GP15 #################################################### # Create Instance ## GPS uart = UART(UART_ID, BAUDRATE, UART_RX, UART_TX) ## OLED SSD1306 i2c = I2C(I2C_ID, scl=Pin(I2C_SCL), sda=Pin(I2C_SDA), freq=I2C_FREQ ) oled = SSD1306_I2C(DISP_W, DISP_H, i2c) ## Panel Switch sw0 = Pin(SW0_GPIO, Pin.IN, Pin.PULL_UP) sw1 = Pin(SW1_GPIO, Pin.IN, Pin.PULL_UP) ## SDCard spi = SPI(SPI_ID, sck=Pin(SPI_SCK), mosi=Pin(SPI_TX), miso=Pin(SPI_RX)) sd = sdcard.SDCard(spi, Pin(SPI_CS)) ### Mouns SDCard os.mount(sd, "/sd") #################################################### # SUB FUNCTION ## Init OLED def clear_disp(disp): disp.fill(0) return #################################################### # MAIN FUNCTION def main(): # Init valid = 0 sentence = "" csv_buf = [] while True: buf = uart.readline() if buf != None: sentence += buf.decode("utf-8") if sentence[len(sentence)-1] == "\n": # Split sentence segments = sentence.split(",") ## Clear sentence = "" ## Check Valid in $GPRMC or GNRMC if segments[0] == "$GPRMC" or segments[0] == "$GNRMC": val = segments[2] if val == "V": valid = 0 gprmc_hour = str((int(segments[1][0:2]) + TIME_ZONE) % 24) gprmc_min = (segments[1][2:4]) gprmc_sec = (segments[1][4:6]) if sw0.value() == 0: oled.poweron() clear_disp(oled) oled.text(f"Not Ready...", 7, 15) oled.text(f"{gprmc_hour}:{gprmc_min}:{gprmc_sec}", 14, 30) oled.show() else: oled.poweroff() pass continue else: valid = 1 dd = int(segments[9][0:2]) MM = int(segments[9][2:4]) yy = int(f"20{segments[9][4:6]}") ## Speed in $GPVTG or $GNVTG if (segments[0] == "$GPVTG" or segments[0] == "$GNVTG") and valid == 1: speed = segments[7] ## Data in $GPGGA or $GNGGA if (segments[0] == "$GPGGA" or segments[0] == "$GNGGA") and valid == 1: hh = int(segments[1][0:2]) mm = int(segments[1][2:4]) ss = int(segments[1][4:6]) utc_datetime = datetime.datetime(yy, MM, dd, hh, mm, ss, tzinfo=datetime.timezone.utc) ### Calc JST jst_datetime = str(utc_datetime.astimezone(datetime.timezone(datetime.timedelta(hours=+9)))) csv_buf.append(jst_datetime[0:10] + "T" + jst_datetime[11:]) ### Add latitude lat = (segments[2][0:2] + "." + segments[2][2:4] \ + segments[2][5:]) csv_buf.append(segments[3]) csv_buf.append(lat) ### Add longitude lon = (segments[4][0:3] + "." + segments[4][3:5] \ + segments[4][6:]) csv_buf.append(segments[5]) csv_buf.append(lon) # Add elevation ele = segments[9] csv_buf.append(ele) ### Add speed csv_buf.append(speed) # Add UTC csv_buf.append(str(utc_datetime)[0:10] + "T" + str(utc_datetime)[11:]) ### Store log file_name = f"{OUTPUT_PATH}{jst_datetime[0:10]}.csv" if sw1.value() == 0: with open(file_name, "a", encoding="utf-8") as f: s_csv_buf = ','.join([str(x) for x in csv_buf]) f.write(s_csv_buf + "\n") else: pass ### Show OLED if sw0.value() == 0: oled.poweron() clear_disp(oled) oled.text(f"{jst_datetime[0:10]}", 0, 0) if sw1.value() == 0: oled.text(f"{jst_datetime[11:19]} GET", 0, 10) else: oled.text(f"{jst_datetime[11:19]} WAIT", 0, 10) oled.text(f"lat: {csv_buf[1]}{csv_buf[2]}", 0, 20) oled.text(f"lon: {csv_buf[3]}{csv_buf[4]}", 0, 30) oled.text(f"spd[km/h]: {csv_buf[6]}", 0, 40) oled.text(f"ele[m]: {csv_buf[5]}", 0, 50) oled.show() else: oled.poweroff() pass ## Clear csv_buf = [] # Clear gc.collect() if __name__ == "__main__": main() """ csv format [0]: JST "20yy-mm-ddTHHMMSS+09:00" [1]: "N"orthern or "S"outhern [2]: Latitude deg [3]: "E"ast or "W"est [4]: Longitude deg [5]: Elevation [m] [6]: Speed [km/h] [7]: UTC "20yy-mm-dd HHMMSS+00:00" """
動作確認
前回作成したZero2と遜色ない動作をすることを確認しました。試作機では006P(9V, 800mAh)を使っていたのですが、約6~7時間程度しか連続稼働しませんでした。
今回は18650型リチウム電池(3.7V, 3400mAh)を使用して、約12時間の連続稼働を確認しました。連続稼働後の電圧はまだまだ余力があり、おそらく24時間程度は連続稼働するのではないかと思います。(未検証)
CSV -> GPX 変換
今回はmicroSDカードにGPSログを保存しているので、同じディレクトリにCSV -> GPX変換のpythonファイルと、バッチファイルを置いておきます。(あらかじめWindowsでPython3が使用できることが前提ですが)
前回のコードとほとんど同じですが、複数ファイルを一度に変換できるように修正しています。
#!/usr/bin/env python3 # encoding: utf-8 # for RPi Pico W # GPS: GT-502MGG-N ## common import os import sys from datetime import datetime as dt import xml.etree.ElementTree as ET import xml.dom.minidom as md import re args = sys.argv INPUT_CSV = f"{args[1]}.csv" OUTPUT_GPX = f"{args[1]}.gpx" VAL_NONE = "." GPX_VER = "1.1" CREATOR = "goldear - https://goldear.net" XMLNS_XSD = "https://www.w3.org/2001/XMLSchema" XMLNS_XSI = "https://www.w3.org/2001/XMLSchema-instance" XSI_SL = "http://www.topografix.com/GPX/1/1/gpx.xsd" #################################### # SUB FUNCTION def get_date(file): timestamp = os.path.getmtime(file) date = dt.fromtimestamp(timestamp) return date def dmm2deg(dmm_lat, dmm_lon): # Convert DMM(Degree Minute) -> DEG(Degree) ## Latitude: decimal -90.0 <= value <=90.0 d_lat = dmm_lat[0:2] int_m_lat = str(dmm_lat[3:5]) sub_m_lat = "." + str(dmm_lat[5:]) float_m_lat = float(int_m_lat + sub_m_lat) m2d_lat = float_m_lat/60 lat = float(d_lat) + m2d_lat deg_lat = str(lat) ## Longitude: decimal -180.0 <= value <=180.0 d_lon = dmm_lon[0:3] int_m_lon = str(dmm_lon[4:6]) sub_m_lon = "." + str(dmm_lon[6:]) float_m_lon = float(int_m_lon + sub_m_lon) m2d_lon = float_m_lon/60 lon = float(d_lon) + m2d_lon deg_lon = str(lon) return deg_lat, deg_lon #################################### # MAIN FUNCTION def csv2gpx(): ## Get time date = get_date(INPUT_CSV) s_date = date.strftime("%y-%m-%d") s_time = date.strftime("%H:%M:%S") ## Create ROOT root = ET.Element("gpx") root.set("version", GPX_VER) root.set("creator", CREATOR) root.set("xmlns:xsd", XMLNS_XSD) root.set("xmlns:xsi", XMLNS_XSI) root.set("xsi:schemaLocation", XSI_SL) ## Set Time l_time = ET.SubElement(root, "time") l_time.text = str("20" + s_date + "T" + s_time + "+09:00") ## Create truck l_trk = ET.SubElement(root, "trk") ## Create truck segment data l_trkseg = ET.SubElement(l_trk, "trkseg") ## Read Log 1-line with open(INPUT_CSV, mode="r", encoding="utf-8") as f_csv: while True: l_gpslog = f_csv.readline() if not l_gpslog: break else: ### split ls_gpslog = re.split("[,*]",l_gpslog) if ls_gpslog[2] == VAL_NONE or ls_gpslog[4] == VAL_NONE: continue ## Create truck point l_trkpt= ET.SubElement(l_trkseg, "trkpt") ## Convert DMM -> DEG lat, lon = dmm2deg(ls_gpslog[2], ls_gpslog[4]) ## Set Lat & Lon l_trkpt.set("lat", lat[0:10] ) l_trkpt.set("lon", lon[0:11] ) ## Create Elevation l_ele = ET.SubElement(l_trkpt, "ele") l_ele.text = ls_gpslog[5] ## Create time l_tr_time = ET.SubElement(l_trkpt, "time") l_tr_time.text = ls_gpslog[0] ## Create speed l_speed = ET.SubElement(l_trkpt, "speed") speed =float(ls_gpslog[6].rstrip()) l_speed.text = f"{speed:.2f}" ## Organize XML XML = md.parseString(ET.tostring(root, "utf-8")) ## Write with open(OUTPUT_GPX, "w") as f: XML.writexml(f, encoding="utf-8", newl="\n", indent="", addindent=" ") if __name__ == "__main__": csv2gpx()
PYTHON_EXE の”user_name”とPythonバージョンは適宜修正することで使用できます。
CSVファイルをバッチファイルにドラッグアンドドロップすることでGPXファイルを生成して、CSVファイルをconvertedディレクトリに移動します。
@echo off REM Convert CSV to GPX REM============================================================================= set PYTHON_EXE="C:\Users\"user_name"\AppData\Local\Programs\Python\Python310\python.exe" set PYTHON_PRG="conv_csv2gpx.py" REM============================================================================= for %%i in (%*) do ( %PYTHON_EXE% %PYTHON_PRG% %%~ni echo on echo %%i echo off ) move *.csv ./converted pause exit
まとめ
試作機5台目にして、まあまあ小型で実用に耐えられるGPSロガーを作成することができました。
今後の課題としては、1. GPSの精度をさらに上げる、 2. ケースの耐久性を上げる、3. 電気的な保護回路を組み込む など挙げていけば切りがありませんが、今後も着々と作っていこうと思います。
コメント