秋月電子通商が販売のGNSSモジュールが出力するシリアルデータを、
WindowsPCへ表示するプログラムをで作成してみた。
<商品名>
GNSS(GPS・GLONASS・QZSS)受信機キット 1PPS出力 みちびき3機対応 アンテナセット付キット
【動作概要】
GNSSデータの中から、GPGGAセンテンスの一部を取り出し、
位置精度を向上させるSBAS受信もアクティブにして(本当に精度UPしたかは不明)、
時刻、緯度、および経度を、ウインドウ表示するアプリ。
【詳細】
添付ソース中の、コメント文に記載
本アプリ動作中のモジュール外しなど、イレギュラー時の対応処理は無い。
【キーワード】
シリアル送受信、スレッド処理のクラス定義、ウインドウ作成/表示、
ボタン作成/表示、ラベル作成/表示、テキストボックス作成/表示
tk.Labelの処理は重くて遅い?
【備忘録】※個人的にPythonやWindowsに関する知識が薄い状態での所見なので、下記の内容は間違いかもしれない。
Tk.Labelの(短時間内の?)頻繁な更新はしない方が良い。
Tk.Labelで頻繁な表示内容の更新をすると、処理がだんだん重くなり、最後は意図しない動作が発生する。
よって、固定文字の表示にTk.Labelを使用し、変化する文字の表示には、Tk.textを使用する。
【注記】
このプログラムの開発者のレベルは、以下の程度
・Windows用ソフト/ドライバに関する知識は、全く無い。
→ Visual_Cのソースを読むことも、ほぼ無理。
・マイコンの組み込み用ソフトは、何個か作ったことがある。
→ C言語、16bitマイコンのI2C/UART/TIMER制御を利用したセンサーモジュールの制御ソフト等
・Python(VisualStudio)+OpenCVで、画像認識ソフトを作成。
→これがウインドウ画面に何かを表示させることができた初めてのソフト。(で、今回が2発目)
→Pythonはtkinterのインポートで簡単にウインドウ表示できるので、
Windowsアプリ作成用言語として採用
【参考文献、サイト】
秋月電子通商のサイト ・・・ モジュールの使用方法を参照
モジュールに搭載の太陽誘電製マイコンのデータシート ・・・ 制御に必要なコマンドを参照
JAXAのサイト ・・・ GNSSとGPSの違い等勉強。(これすら、本アプリの開発者は知らなかった。)
【接続状況 と 動作画面】
【GNSS受信モジュール の設定】
UART通信速度、GPGGAセンテンスの取り出し、ACKパケット について以下に記載
UART通信速度の設定 (82行目)
<データシート抜粋>
251 PMTK_SET_NMEA_BAUDRATE
Data Field: $PMTK251,Baudrate
Baud rate: Baud rate setting "38400" = 38400bps
<今回の設定値>
UART_38400 = b"$PMTK251,38400*27\r\n"
説明)
ボーレート 38400bps
*27 は、”P”から”0”までのXOR値 (下表)
GPGGAデータのみの取り出し (86行目)
<データシート抜粋>
314 PMTK_API_SET_NMEA_OUTPUT
Supported NMEA Sentences
0 ・・・
1 ・・・
2 ・・・
3 NMEA_SEN_GGA, // GPGGA interval-GPS Fix data
4 ・・・
・・・
Supported Frequency Setting
0 ・・・
1 Output once every one position fix.
2 ・・・
・・・
<今回の設定値>
PMTK_GGA_OUT = b"$PMTK314,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0*29\r\n"
説明)
3 (0番目から数えて3番目)だけ 1 をセット(他GPGLL等は”0”に)したので、
GPGGAのデータを、1個のポジションデータ確定で出力する設定 とした。
*29 は、”P”から”0”までのXOR値 (下表)
【ACKパケットについて】
受信モジュールにモジュール設定値を送信すると、ACKパケットを返信してくる。(注1)
受信モジュールが返信するACKパケットについて、下記に示す。
$PMTK001,xxx,Y*XOR <CR><LF>
PMTK001 : ACKのパケット番号(”001”)。
xxx:受信モジュールへ送信したパケット番号を示す。
Y:xxxのパケットを正常に受信("3")したか、失敗したか("0"or"1"or"2")を示す。
XOR:"P"からYまでのXOR値 (本プログラムでは、XOR値は無視)
(注1) 通信速度設定時(PMTK251)には、そのACKパケットの返信は確認できなかった。($PMTK001,251)
ソースリスト
- # -*- coding: utf-8 -*-
- “””
- 【目的】
- 受信したGNSSデータを WindowsPCへ表示する。
- 【使用条件】
- 日本国内のみ (テストは東京都内のみで実施)
- 【プログラム概要】
- ———————————————————————————
- ・GNSSモジュールをGPGGAデータのみの出力に設定する。
- ・GPGGAデータから0.3秒毎に出力される、時間と緯度/経度データを取り込み、表示する。
- ・時間データは、日本時間へ換算する。(UTC+9H)
- ・緯度は、北緯(N)、経度は、東経(E)のみの表示 ※日本国内での使用を想定
- ・GNSSモジュールから出力される緯度と経度データを 10進表記に換算し表示する。(123度45.123分 → 123.753416度))
- → 表示される北緯/東経の値を、google_earthの検索BOXに入力すると、現在位置が図示される。
- ・RUNボタンで、表示開始、 Waitボタンで、表示の更新を中断
- -実行結果-
- <表示>
- ・RUN Wait
- Original GPGGA,xxxxxxxxxxxxxxxxxxxxx,N,xxxxxxx,E
- TIME Hh : Mm : Ss
- North 12.3456
- East 78.9012
- ———————————————————————————
- 【開発環境】
- ———————————————————————————
- PC: i5-9600KF, Z390, 16GB, GTX1660S+GT1030
- GNSS受信モジュール : 秋月製 K-13849
- シリアル-USB変換モジュール : メーカー不明 ※5V対応品
- OS : Windows10-Pro
- IDE: Visual Studio 2017
- 言語 : python 3.6
- ———————————————————————————
- 【機器接続】
- ———————————————————————————
- <<<PC>>>(USB2.0) —– (USB2.0)<<シリアル-USB変換モジュール>>(5V/Tx/Rx/GND) —– (5V/Rx/Tx/GND)<<GNSS受信モジュール>>
- ———————————————————————————
- 【電源】
- ———————————————————————————
- 5V ※ PCからシリアル-USB変換モジュールを経由し、GNSS受信モジュールへ供給
- ———————————————————————————
- 【ソフトウェア設定】
- ———————————————————————————
- UART: 38400bps( | 9600bps) データ長8bit ストップビット有り パリ無し フロー制御無し
- 注) 通信レート変更時は、変更前のレート値(既にモジュールに設定されている値)で、変更したいレート値に設定すること。
- ———————————————————————————
- 【精度】(条件:38400bps、フレーム更新間隔0.3秒)
- ———————————————————————————
- 表示時刻ズレ
- 設計値 : 1秒未満
- 実測値 : 2秒以内 (Windows10の時計(NTP時刻)とのズレを目視で確認)
- 位置ズレ (SBAS使用)
- 設計値 : ?
- 実測値 : 最小5m程度
- ———————————————————————————
- 【キーワード】 ・・・ 今後、プログラムを自作する際のコピペと参考用に、今回使用したpythonの機能などを記載。
- シリアル送受信、スレッド処理のクラス定義、ウインドウ作成/表示、ボタン作成/表示、ラベル作成/表示、テキストボックス作成/表示
- “””
- import tkinter
- #from tkinter import messagebox
- #import numpy as np
- #import cv2
- import serial
- import binascii
- #import time
- import threading
- #from threading import (Event, Thread)
- #UART_9600 = b”$PMTK251,9600*17\r\n” # ボーレート 9600bps ※ACK無し
- UART_38400 = b”$PMTK251,38400*27\r\n” # ボーレート 38400bps ※ACK無し
- SET_INTERVAL_300MS = b”$PMTK220,300*2D\r\n” # 受信情報更新間隔 0.3S ※38400bps以上で設定可
- #SET_INTERVAL_1000MS = b”$PMTK220,1000*1F\r\n” # 受信情報更新間隔 1S
- SBAS_ENABLE = b”$PMTK313,1*2E\r\n” # SBAS受信_ON
- PMTK_GGA_OUT = b”$PMTK314,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0*29\r\n” # GPGGAセンテンスのみ出力
- serial_dat = 0
- RUN = 1
- PAUSE = 0
- OK = 0
- NG = 1
- # ACK受信処理 ※UART通信レート以外の設定は、ACK応答の比較を行う
- def ACK_Receive(num):
- ser.reset_input_buffer() # 受信バッファクリア
- # 先頭文字”$”を受信するまで 繰り返し受信
- while True:
- serial_dat = ser.read() # シリアルデータを1byte受信
- if serial_dat.decode(“UTF-8”) == ‘$’:
- break
- serial_dat = ser.readline() # ASCII制御コードLF(\n)まで シリアルコードを受信
- print(serial_dat.decode(“UTF-8”)) #debug ACKコードを表示
- if num == 1:
- if serial_dat[0:13] == b”PMTK001,220,3″: # 期待値のACKコードと比較
- return OK
- elif num == 2:
- if serial_dat[0:13] == b”PMTK001,313,3″: # 期待値のACKコードと比較
- return OK
- elif num == 3:
- if serial_dat[0:13] == b”PMTK001,314,3″: # 期待値のACKコードと比較
- return OK
- return NG
- # 受信モジュールへコマンド送信処理 と ACK応答待ち
- def GNSS_command():
- for i in range(4):
- while ser.out_waiting != 0: {} # 送信完了待ち
- if i == 0:
- ser.write(UART_38400); # UART通信レートを設定 ※この設定のACK応答待ちは行わない。(->モジュールからのACK応答が確認できない)
- elif i == 1:
- while ACK_Receive(i) == NG:
- ser.write(SET_INTERVAL_300MS); # GNSSデータ転送間隔を設定
- print(‘SET_INTERVAL ACK(PMTK001,220,3) -> NG’) #debug
- print(‘SET_INTERVAL ACK(PMTK001,220,3) -> OK’) #debug
- elif i == 2:
- while ACK_Receive(i) == NG:
- ser.write(SBAS_ENABLE) # SBASの受信を許可
- print(‘SBAS ACK(PMTK001,313,3) -> NG’) #debug
- print(‘SBAS ACK(PMTK001,313,3) -> OK’) #debug
- elif i == 3:
- while ACK_Receive(i) == NG:
- ser.write(PMTK_GGA_OUT) # GPGGAデータのみの出力へ設定
- print(‘GPGGA_OUT ACK(PMTK001,314,3) -> NG’) #debug
- print(‘GPGGA_OUT ACK(PMTK001,314,3) -> OK’) #debug
- #RUNボタンの定義
- def click_Run_button():
- gnss_thread.thread_state = RUN #GNSSデータ受信開始
- # Pauseボタンの定義
- def click_Pause_button():
- gnss_thread.thread_state = PAUSE #GNSSデータ受信中断 ※シリアルポートは開いたままで、受信データは未使用。
- #GNSSデータの受信 ・・・ スレッド処理
- class GNSS_Data_Receive(threading.Thread):
- def __init__(self):
- threading.Thread.__init__(self)
- self.thread_state = PAUSE
- # テキストボックスを4つ作成。 シリアルデータとその換算値は、このテキストボックス内に表示される。
- input_label = tkinter.Label(text = “Original “)
- input_label.place(x=1, y=70)
- self.text_gpgga = tkinter.Text(root_window, width=50, height=1)
- self.text_gpgga.place(x = 70, y=70)
- input_label = tkinter.Label(text = “TIME “)
- input_label.place(x=1, y=100)
- self.text_time = tkinter.Text(root_window, width=50, height=1)
- self.text_time.place(x = 70, y=100)
- input_label = tkinter.Label(text = “North “)
- input_label.place(x=1, y=130)
- self.text_latitude = tkinter.Text(root_window, width=50, height=1)
- self.text_latitude.place(x = 70, y=130)
- input_label = tkinter.Label(text = “East”)
- input_label.place(x=1, y=160)
- self.text_longitude = tkinter.Text(root_window, width=50, height=1)
- self.text_longitude.place(x = 70, y=160)
- def run(self):
- while True: # スレッド処理の繰り返し ※これが無いと、Waitボタンを押した後、RUNボタンを押しても再開しない。
- while self.thread_state == RUN :
- ser.reset_input_buffer() # 受信バッファクリア
- serial_dat = ser.read() # シリアルデータを1byte受信
- if serial_dat.decode(“UTF-8”) == ‘$’ : # 受信した1バイトのデータが、GPGGAデータの先頭文字”$”なら、残りの41バイトを取り込む
- serial_dat = ser.read(41) # シリアルデータを41byte受信
- header = serial_dat[0:5] # 先頭5文字(”GPGGA”)を取得
- serial_dat = serial_dat.decode(“UTF-8”) # byte型をstr型へ変換
- header = header.decode(“UTF-8”) # byte型をstr型へ変換
- # GPSモジュールが出力するオリジナルデータの表示
- self.text_gpgga.delete(“1.0”, “end”) # 対象行の表示削除
- self.text_gpgga.insert(tkinter.END, str(serial_dat)) # GPGGAデータの表示 ※ ”$”は非表示、42バイト目移行も非表示
- # 時刻のバイトデータを数値(int型)データへ変換し、表示/更新
- time_hour = int(serial_dat[6:8]) + 9 # 世界標準時(UTC)を 日本標準時(JST)へ補正 (+9時間)
- if time_hour >= 24:
- time_hour -= 24
- self.text_time.delete(“1.0”, “end”) # 対象行の表示削除
- self.text_time.insert(tkinter.END, str(time_hour)+” : ” + serial_dat[8:10] + ” : ” + serial_dat[10:12]) # 時刻の表示
- #緯度のバイトデータを数値(int型)データへ変換し、表示/更新 ※21byte目の小数点を削除。 NMEA表記(xx度yy.zz分)を10進表記(xx.aaa度)に変換
- latitude_upper = int(serial_dat[17:19])
- latitude_lower = (int(serial_dat[19:21])*10000 + int(serial_dat[22:26]))/600000
- latitude = latitude_upper + latitude_lower
- #print(str(latitude)) # debug
- self.text_latitude.delete(“1.0”, “end”) # 対象行の表示削除
- self.text_latitude.insert(tkinter.END, str(latitude)) # 北緯の表示
- #経度のバイトデータを数値(int型)データへ変換し、表示/更新 ※34byte目の小数点を削除。 NMEA表記(xx度yy.zz分)を10進表記(xx.aaa度)に変換
- longitude_upper = int(serial_dat[29:32])
- longitude_lower = (int(serial_dat[32:34])*10000 + int(serial_dat[35:39]))/600000
- longitude = longitude_upper + longitude_lower
- #print(str(longitude)) # debug
- self.text_longitude.delete(“1.0”, “end”) # 対象行の表示削除
- self.text_longitude.insert(tkinter.END, str(longitude)) # 東経の表示
- “”” tkinter.Label(と.placeの組み合わせ)を(繰り返し)使うと、10分以内に表示が崩れるので、以下の処理は未使用。 (5分でカクつく、10分以内に表示が崩れる)
- #時刻、緯度と経度をウィンドウへ表示
- input_label = tkinter.Label(text = ” “, font = 16) # 一度文字を消す
- input_label.place(x=1, y=110)
- input_label = tkinter.Label(text = “TIME ” + str(time_hour) + ” : ” + serial_dat[8:10] + ” : ” + serial_dat[10:12] , font = 16)
- input_label.place(x=1, y=110)
- input_label = tkinter.Label(text = ” “, font = 16) # 一度文字を消す
- input_label.place(x=1, y=150)
- input_label = tkinter.Label(text = “北緯 ” + str(latitude) , font = 16)
- input_label.place(x=1, y=150)
- input_label = tkinter.Label(text = ” “, font = 16) # 一度文字を消す
- input_label.place(x=1, y=190)
- input_label = tkinter.Label(text = “東経 ” + str(longitude) , font = 16)
- input_label.place(x=1, y=190)
- “””
- #######################################################
- ################### ここからメイン #########################
- # シリアルポートの定義とオープン
- ser = serial.Serial(
- port = ‘COM4’,
- baudrate = 38400,
- parity = serial.PARITY_NONE,
- bytesize = serial.EIGHTBITS,
- stopbits = serial.STOPBITS_ONE,
- timeout = None,
- xonxoff = 0,
- rtscts = 0,
- )
- #シリアルポートのバッファクリア
- ser.reset_input_buffer() # 受信バッファクリア
- ser.reset_output_buffer() # 送信バッファクリア
- # GNSSモジュールの設定
- while GNSS_command() == NG: {}
- # 表示ウィンドウの作成
- root_window = tkinter.Tk()
- root_window.title(“GNSS GPAGGA”)
- root_window.geometry(“500×240”)
- #ボタンの作成 2ヶ
- Run_button0=tkinter.Button(root_window, width=5, text=”Run”, command=lambda:click_Run_button()) #シンプル表示
- #Run_button0=tkinter.Button(root_window, width=5, background=”#0000ff”, fg = ‘#ffffff’, text=”Run”, command=lambda:click_Run_button())
- Run_button0.place(x=30, y=10)
- Pause_button0=tkinter.Button(root_window, width=5, text=”Wait”, command=lambda:click_Pause_button()) #シンプル表示
- #Pause_button0=tkinter.Button(root_window, width=5, background=”#ff0000″, fg = ‘#000000’, text=”Wait”, command=lambda:click_Pause_button())
- Pause_button0.place(x=100, y=10)
- #スレッド処理で、GNSS_Data_Receive関数を並行して実行
- gnss_thread = GNSS_Data_Receive()
- gnss_thread.start() #スレッド処理開始
- # 繰り返し実行
- root_window.mainloop()
- ser.close()
- #cap.release()
- #cv2.destroyAllWindows()
- “””
- 開発履歴 (やってみたことの概要)
- <1>
- メイン: メインウインドウの生成、ボタンの生成
- スレッド: シリアルデータ受信、ラベルの表示/更新
- 結果: 5分でもたつく。10分以内に、表示が崩れる。
- 検討: ラベルで表示する代わりに、コマンド画面に表示させた場合、10分経過でももたつき無し。 このことから、シリアル受信処理が原因では無い様子。
- <2> ・・・ 概要 : <1>をベースに、表示の更新頻度を下げてみた。— 小変更
- メイン: メインウインドウの生成、ボタンの生成
- スレッド: シリアルデータ受信、ラベルの表示/更新。 シリアル出力周期を最大限下げ(300mS→1000mS)、ラベルの更新頻度を下げてみた。
- 結果: 10分以内にもたつき始める。15分程度で、表示が崩れる。
- 検討: まだ、ラベルの表示/更新処理の負荷が高い状態。
- <3> ・・・ 概要 : <1>をベースに、メインとスレッドで担当する処理を入れ替えた。— 大変更
- メイン: シリアルデータ受信、ラベルの表示/更新
- スレッド: メインウインドウの生成、ボタンの生成
- 結果: 5分でもたつく。10分以内に、表示が崩れる。 -> <1>と同じ
- メインもスレッドも処理能力は、同程度と判断。
- 更新データの表示方法の再検討が必要。
- 検討:
- Tk.Labelに関して調査/検索した結果、この処理は負荷が高く、頻繁に繰り返すと処理が追いつかなくなる
- 様なので、Tk.Textでシリアルデータを表示する方式に切り換えてみる。
- → 但し、tk.text の方が軽処理である根拠はない、が、
- ラベルは、タイトル文字の様に、更新しない文字を表示するためのもので、
- text(テキストボックス)は、ユーザーの打ち込む文字など、頻繁な更新と即時反映を想定した処理になっているかも・・・と考えた。
- <4> ・・・ 概要 : <1>をベースに、テキストボックスに表示する処理へ変更した。— 中変更
- メイン: メインウインドウの生成、ボタンの生成
- スレッド: シリアルデータ受信、テキストボックスの表示/更新 ※シリアル出力周期は、0.3秒
- 結果: 40分経過後も、カクつき/表示異常無し なので、<4>のテキストボックス表示方式を採用
- “””
コメント