見出し画像

GPSスピードメーターとGPSロガーをRaspberry Piで作ったよ

はじめに

この記事は「NBUゆるゆるかれんだー Advent Calendar 2022」用に執筆しました。

こんにちは、ぶたのけいとんです。

今回は、GPSスピードメーターGPSロガーRaspberry Piで作ったので、その過程を画像多めで紹介していきます。

タイトルがなんじゃそりゃ?の人向け

GPSスピードメーターとは、GPS(Global Positioning System・全地球測位システム)で測位した位置情報(緯度・経度など)を用いて速度を算出するタイプのスピードメーターです。

GPSロガーとは、GPSで測位した位置情報記録する装置です。ヘンゼルとグレーテルの道しるべの現代版みたいな感じですかね?

Raspberry Piとは、安い小さい高性能が売りなコンピュータです。Linuxが動きます。最近はコロナ過で入手困難です。でも2023年後半から解消されるらしいですよ。

サンタさん!ありがとう!!

ある日の朝

靴下の中に何か入っています。サンタさんが来られたようです。何が入っているのでしょう。

中に入っていたのは、左からRaspberry Pi Zero 2 WAE-GPS(秋月電子のGPS受信モジュール)AE-BME280(秋月電子の温湿度・気圧センサモジュール)ST7735(1.8インチフルカラー128×160 TFT LCDディスプレイモジュール)です。

お手紙も入っていました。

なんということでしょう。サンタさんが「部品あげるからGPSスピードメーターGPSロガーを作れ」と囁いているではありませんか。後日、銀行残高が減っていたんですけどね。

スピードメーター編

楽しい工作タイム

それでは作っていきます。

段ボールをチョキチョキします。

筐体が完成。Amazon!

Raspberry Piを取り付け。

GPSモジュールと温湿度・気圧センサーモジュールを取り付け。

ディスプレイを取り付けて、配線。

完成!

楽しいプログラミングタイム

スピードメーターのコードをPythonで書いていきます。

import time
import digitalio
import board
from PIL import Image, ImageDraw, ImageFont
from adafruit_rgb_display import st7735
from serial import Serial
from micropyGPS import MicropyGPS
import threading
import smbus
from decimal import Decimal, ROUND_HALF_UP

# Configuration for CS and DC pins (these are PiTFT defaults):
cs_pin = digitalio.DigitalInOut(board.CE0)
dc_pin = digitalio.DigitalInOut(board.D25)
reset_pin = digitalio.DigitalInOut(board.D24)

# Config for display baudrate (default max is 24mhz):
BAUDRATE = 24000000

# Setup SPI bus using hardware SPI:
spi = board.SPI()

# Create the display:
disp = st7735.ST7735R(
    spi,
    rotation=270,
    x_offset=2,
    y_offset=1,
    bgr=True,
    cs=cs_pin,
    dc=dc_pin,
    rst=reset_pin,
    baudrate=BAUDRATE,
)

# Create blank image for drawing.
# Make sure to create image with mode 'RGB' for full color.
if disp.rotation % 180 == 90:
    height = disp.width  # we swap height/width to rotate it to landscape!
    width = disp.height
else:
    width = disp.width  # we swap height/width to rotate it to landscape!
    height = disp.height

image = Image.new("RGB", (width, height))

# Get drawing object to draw on image.
draw = ImageDraw.Draw(image)

# Draw a black filled box to clear the image.
draw.rectangle((0, 0, width, height), outline=0, fill=(0, 0, 0))
disp.image(image)

# First define some constants to allow easy positioning of text.
padding = 0
x = 0

# Load a TTF font.  Make sure the .ttf font file is in the
# same directory as the python script!
# Some other nice fonts to try: http://www.dafont.com/bitmap.php
font = ImageFont.truetype("/usr/share/fonts/truetype/fonts-japanese-gothic.ttf", 18)

TIME_ZONE = 9 # タイムゾーン(UTF+TIME_ZOME)

gps = MicropyGPS(TIME_ZONE, 'dd') # MicroGPSオブジェクトを生成する。(引数はタイムゾーンの時差と出力フォーマット)

gps_serial = Serial('/dev/serial0', 9600, timeout=10)

bus_number = 1
i2c_address = 0x76

bus = smbus.SMBus(bus_number)

digT = []
digP = []
digH = []

t_fine = 0.0

def run_gps(): # GPSモジュールを読み、GPSオブジェクトを更新する
    while True:
        try:
            sentence = gps_serial.readline().decode('utf-8') # GPSデーターを読み、文字列に変換する
        except UnicodeDecodeError:
            print('Decode Error')
            continue
        if sentence[0] != '$': # 先頭が'$'でなければ捨てる
            print('Not Matched $')
            continue
        for x in sentence: # 読んだ文字列を解析してGPSオブジェクトにデーターを追加、更新する
            gps.update(x)

def writeReg(reg_address, data):
	bus.write_byte_data(i2c_address,reg_address,data)

def get_calib_param():
	calib = []
	
	for i in range (0x88,0x88+24):
		calib.append(bus.read_byte_data(i2c_address,i))
	calib.append(bus.read_byte_data(i2c_address,0xA1))
	for i in range (0xE1,0xE1+7):
		calib.append(bus.read_byte_data(i2c_address,i))

	digT.append((calib[1] << 8) | calib[0])
	digT.append((calib[3] << 8) | calib[2])
	digT.append((calib[5] << 8) | calib[4])
	digP.append((calib[7] << 8) | calib[6])
	digP.append((calib[9] << 8) | calib[8])
	digP.append((calib[11]<< 8) | calib[10])
	digP.append((calib[13]<< 8) | calib[12])
	digP.append((calib[15]<< 8) | calib[14])
	digP.append((calib[17]<< 8) | calib[16])
	digP.append((calib[19]<< 8) | calib[18])
	digP.append((calib[21]<< 8) | calib[20])
	digP.append((calib[23]<< 8) | calib[22])
	digH.append( calib[24] )
	digH.append((calib[26]<< 8) | calib[25])
	digH.append( calib[27] )
	digH.append((calib[28]<< 4) | (0x0F & calib[29]))
	digH.append((calib[30]<< 4) | ((calib[29] >> 4) & 0x0F))
	digH.append( calib[31] )
	
	for i in range(1,2):
		if digT[i] & 0x8000:
			digT[i] = (-digT[i] ^ 0xFFFF) + 1

	for i in range(1,8):
		if digP[i] & 0x8000:
			digP[i] = (-digP[i] ^ 0xFFFF) + 1

	for i in range(0,6):
		if digH[i] & 0x8000:
			digH[i] = (-digH[i] ^ 0xFFFF) + 1  

def getData():
	data = []
	for i in range (0xF7, 0xF7+8):
		data.append(bus.read_byte_data(i2c_address,i))
	pres_raw = (data[0] << 12) | (data[1] << 4) | (data[2] >> 4)
	temp_raw = (data[3] << 12) | (data[4] << 4) | (data[5] >> 4)
	hum_raw  = (data[6] << 8)  |  data[7]
		
	return pres_raw, temp_raw, hum_raw

def compensate_P(adc_P):
	global  t_fine
	pressure = 0.0
	
	v1 = (t_fine / 2.0) - 64000.0
	v2 = (((v1 / 4.0) * (v1 / 4.0)) / 2048) * digP[5]
	v2 = v2 + ((v1 * digP[4]) * 2.0)
	v2 = (v2 / 4.0) + (digP[3] * 65536.0)
	v1 = (((digP[2] * (((v1 / 4.0) * (v1 / 4.0)) / 8192)) / 8)  + ((digP[1] * v1) / 2.0)) / 262144
	v1 = ((32768 + v1) * digP[0]) / 32768
	
	if v1 == 0:
		return 0
	pressure = ((1048576 - adc_P) - (v2 / 4096)) * 3125
	if pressure < 0x80000000:
		pressure = (pressure * 2.0) / v1
	else:
		pressure = (pressure / v1) * 2
	v1 = (digP[8] * (((pressure / 8.0) * (pressure / 8.0)) / 8192.0)) / 4096
	v2 = ((pressure / 4.0) * digP[7]) / 8192.0
	pressure = pressure + ((v1 + v2 + digP[6]) / 16.0)  

	return pressure

def compensate_T(adc_T):
	global t_fine
	v1 = (adc_T / 16384.0 - digT[0] / 1024.0) * digT[1]
	v2 = (adc_T / 131072.0 - digT[0] / 8192.0) * (adc_T / 131072.0 - digT[0] / 8192.0) * digT[2]
	t_fine = v1 + v2
	temperature = t_fine / 5120.0

	return temperature

def compensate_H(adc_H):
	global t_fine
	var_h = t_fine - 76800.0
	if var_h != 0:
		var_h = (adc_H - (digH[3] * 64.0 + digH[4]/16384.0 * var_h)) * (digH[1] / 65536.0 * (1.0 + digH[5] / 67108864.0 * var_h * (1.0 + digH[2] / 67108864.0 * var_h)))
	else:
		return 0
	var_h = var_h * (1.0 - digH[0] * var_h / 524288.0)
	if var_h > 100.0:
		var_h = 100.0
	elif var_h < 0.0:
		var_h = 0.0

	return var_h


def setup():
	osrs_t = 1			#Temperature oversampling x 1
	osrs_p = 1			#Pressure oversampling x 1
	osrs_h = 1			#Humidity oversampling x 1
	mode   = 3			#Normal mode
	t_sb   = 5			#Tstandby 1000ms
	filter = 0			#Filter off
	spi3w_en = 0			#3-wire SPI Disable

	ctrl_meas_reg = (osrs_t << 5) | (osrs_p << 2) | mode
	config_reg    = (t_sb << 5) | (filter << 2) | spi3w_en
	ctrl_hum_reg  = osrs_h

	writeReg(0xF2,ctrl_hum_reg)
	writeReg(0xF4,ctrl_meas_reg)
	writeReg(0xF5,config_reg)


gps_thread = threading.Thread(target=run_gps, args=()) # 上の関数を実行するスレッドを生成
gps_thread.daemon = True
gps_thread.start() # スレッドを起動

setup()
get_calib_param()

try:
    while True:
        h = gps.timestamp[0] if gps.timestamp[0] < 24 else gps.timestamp[0] - 24
        print('%2d年 %2d月 %2d日 %2d時 %02d分 %02d秒' % (gps.date[2], gps.date[1], gps.date[0], h, gps.timestamp[1], gps.timestamp[2]))
        print('緯度経度: %2.8f, %2.8f' % (gps.latitude[0], gps.longitude[0]))
        print('海抜: %f [m]' % gps.altitude)
        print('速度: %f [km/h]' % gps.speed[2])
        print('=' * 40)

        show_date = '%2d年%2d月%2d日' % (gps.date[2], gps.date[1], gps.date[0])
        show_time = '%2d時%02d分%02d秒' % (h, gps.timestamp[1], gps.timestamp[2])
        show_speed = "速度:%6.1f km/h" % float(Decimal(str(gps.speed[2])).quantize(Decimal('0.1'), rounding=ROUND_HALF_UP))
        show_above_sea_level = "海抜:%6.1f m" % float(Decimal(str(gps.altitude)).quantize(Decimal('0.1'), rounding=ROUND_HALF_UP))


        pres_raw, temp_raw, hum_raw = getData()
        show_temp = "気温:%6.1f ℃" % float(Decimal(str(compensate_T(temp_raw))).quantize(Decimal('0.1'), rounding=ROUND_HALF_UP))
        show_hum = "湿度:%6.1f %" % float(Decimal(str(compensate_H(hum_raw))).quantize(Decimal('0.1'), rounding=ROUND_HALF_UP))
        show_pressure = "気圧:%6.2fhPa" % float(Decimal(str(compensate_P(pres_raw) / 100)).quantize(Decimal('0.01'), rounding=ROUND_HALF_UP))

        # Draw a black filled box to clear the image.
        draw.rectangle((0, 0, width, height), outline=0, fill=0)

        # Write four lines of text
        y = padding
        draw.text((x, y), show_date, font=font, fill="#FFFFFF")
        y += font.getsize(show_date)[1]
        draw.text((x, y), show_time , font=font, fill="#FFFFFF")
        y += font.getsize(show_time)[1]
        draw.text((x, y), show_speed , font=font, fill="#FF8800")
        y += font.getsize(show_speed)[1]
        draw.text((x, y), show_above_sea_level , font=font, fill="#FFFF00")
        y += font.getsize(show_above_sea_level)[1]
        draw.text((x, y), show_temp , font=font, fill="#00FF00")
        y += font.getsize(show_temp)[1]
        draw.text((x, y), show_hum , font=font, fill="#00FFFF")
        y += font.getsize(show_hum)[1]
        draw.text((x, y), show_pressure , font=font, fill="#0088FF")

        # Display image.
        disp.image(image)
        time.sleep(1)
except KeyboardInterrupt:
    pass

ほとんどコピペです。しかし、ディスプレイの細かい仕様の違いに対応するのに苦戦しました。あと、浮動小数点数を正確に四捨五入して文字列に変換するとか、スレッドの中に例外処理入れないとエラーで止まるとか、なんやかんやでプログラミングが一番時間がかかりました

完成!
海抜が異常値出してますが。

プログラミングは、以下のサイトを参考にしました。


いざ試走!

自転車に乗って動作チェック。
外だと画面が見ずらいですが、20.7km/hになっています。体感もそのくらいだったので精度も良いと思います。

以上でスピードメーターは完成!
次はGPSロガーです。

GPSロガー編

実装

GPSロガーには「gpxlogger」というソフトウェアを使います。gpsdかgpsd-clientsのどちらかに付属しています。

gpxlogger -f gps.gpx

このコマンドを打つだけで、gps.gpxというファイルにGPXというフォーマットで位置情報が書き込まれていきます。

さあ、GPSロガーを起動しようと思ったら、ここで問題発生。

先ほどのスピードメーターのPythonプログラムを走らせながら上記のコマンドを実行すると、エラーが出てしまいます。

「GPS is busy」的なメッセージが出ていました。
GPSを同時に2つのプログラムから使えないのでしょうか。

二兎を追う者は一兎しか得ずってことですかね。
この辺の問題は難しそうなので、手っ取り早くこうしました。

「GPS Speedometer & GPS Logger」→「GPS Speedometer XOR GPS Logger」

つまり、スピードメーターかGPSロガーのどちらかしか使えないという仕様に変更です。

GPSロガー実行中は、ディスプレイが暇になるので寿司の画像を表示しておきます。最終的には爆弾になるんですけどね。

よりコンパクトに

これが爆弾です。

嘘です。

GPSロガーです。

ディスプレイが不要なので取り外して、筐体をプラスチックに変更モバイルバッテリーを内蔵式にしました。

持ち運ぶにはこれくらいじゃないと。
さっきの段ボールでは無駄に大きくて億劫です。

にしても、「基盤丸見え&赤いLEDが点滅」は爆弾感があります。
電車で手に持ってたらやばいですね。

いざ記録!

それでは、先ほどのコンパクトになったGPSロガーを持って位置情報を記録していきます。

しかし、ただ記録するだけでは面白くないですよね。
軌跡を何か面白い形にしたいじゃないですか。

選ばれたのは、💩でした。

キャンバスは夜の稲沢公園。

いえてぃーと一緒に💩の形に歩きます。

不具合

記録が終わり、家に帰ってログを確認してみると、

記録できてない!!(泣)

何のために夜の稲沢公園で💩の形に歩いてたんだ、、、

💩が水の泡に、、、
いや、それがしかるべきストーリー。
トイレに流してなんぼですもんね。

記録できていない原因はわかりません。

💩なんて描いたから天罰が下ったんでしょうね。

そんなこともあろうかと、スマホでもログをとっていたので載せておきます。

というわけで、GPSロガーの方は今後、要改良です。

まとめ

今回、GPSスピードメーターとGPSロガーを作って思ったことは、開発の楽しさを再発見出来たことです。

と、なんかそれっぽいことを言ってしまいましたが、今までWebアプリなどを作っていた時とは違う楽しさがありました。

特に今回は、GPSを使うという特性上、家の外で検証しなければなりません。歩き回って検証するのが、今までと違ってなんとなくワクワクしたのだと思います。

以上で終わりです。
最後までご清覧いただき、ありがとうございました。



この記事が気に入ったらサポートをしてみませんか?