GNSSデータ表示ソフトの作成

Hardware & Software
Pocket

秋月電子通商が販売の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受信モジュール(緑)
 + USB/シリアル変換基板(赤)
動作中(RUNボタン押し後)の画面
【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)
ソースリスト

 

  1. # -*- coding: utf-8 -*-
  2.  
  3. “””
  4. 【目的】
  5.     受信したGNSSデータを WindowsPCへ表示する。
  6.  
  7. 【使用条件】
  8.     日本国内のみ (テストは東京都内のみで実施)
  9.  
  10. 【プログラム概要】
  11.     ———————————————————————————
  12.     ・GNSSモジュールをGPGGAデータのみの出力に設定する。
  13.     ・GPGGAデータから0.3秒毎に出力される、時間と緯度/経度データを取り込み、表示する。
  14.     ・時間データは、日本時間へ換算する。(UTC+9H)
  15.     ・緯度は、北緯(N)、経度は、東経(E)のみの表示 ※日本国内での使用を想定
  16.     ・GNSSモジュールから出力される緯度と経度データを 10進表記に換算し表示する。(123度45.123分 → 123.753416度))
  17.       → 表示される北緯/東経の値を、google_earthの検索BOXに入力すると、現在位置が図示される。
  18.     ・RUNボタンで、表示開始、 Waitボタンで、表示の更新を中断
  19.  
  20.     -実行結果-
  21.     <表示>
  22.         ・RUN Wait
  23.          Original        GPGGA,xxxxxxxxxxxxxxxxxxxxx,N,xxxxxxx,E
  24.          TIME            Hh : Mm : Ss
  25.          North        12.3456
  26.          East            78.9012
  27.     ———————————————————————————
  28.  
  29. 【開発環境】
  30.     ———————————————————————————
  31.     PC:                                    i5-9600KF, Z390, 16GB, GTX1660S+GT1030
  32.     GNSS受信モジュール     :            秋月製 K-13849
  33.     シリアル-USB変換モジュール :    メーカー不明 ※5V対応品
  34.     OS :                                    Windows10-Pro
  35.     IDE:                                    Visual Studio 2017
  36.     言語 :                                python 3.6
  37.     ———————————————————————————
  38.  
  39. 【機器接続】
  40.     ———————————————————————————
  41.     <<<PC>>>(USB2.0) —– (USB2.0)<<シリアル-USB変換モジュール>>(5V/Tx/Rx/GND) —– (5V/Rx/Tx/GND)<<GNSS受信モジュール>>
  42.     ———————————————————————————
  43.  
  44. 【電源】
  45.     ———————————————————————————
  46.     5V  ※ PCからシリアル-USB変換モジュールを経由し、GNSS受信モジュールへ供給
  47.     ———————————————————————————
  48.  
  49. 【ソフトウェア設定】
  50.     ———————————————————————————
  51.     UART:                 38400bps( | 9600bps)  データ長8bit ストップビット有り パリ無し フロー制御無し
  52.     注) 通信レート変更時は、変更前のレート値(既にモジュールに設定されている値)で、変更したいレート値に設定すること。
  53.     ———————————————————————————
  54.  
  55. 【精度】(条件:38400bps、フレーム更新間隔0.3秒)
  56.     ———————————————————————————
  57.     表示時刻ズレ
  58.         設計値     : 1秒未満
  59.         実測値    : 2秒以内 (Windows10の時計(NTP時刻)とのズレを目視で確認)
  60.     位置ズレ (SBAS使用)
  61.         設計値     : ?
  62.         実測値 : 最小5m程度
  63.     ———————————————————————————
  64.  
  65. 【キーワード】 ・・・ 今後、プログラムを自作する際のコピペと参考用に、今回使用したpythonの機能などを記載。
  66.  シリアル送受信、スレッド処理のクラス定義、ウインドウ作成/表示、ボタン作成/表示、ラベル作成/表示、テキストボックス作成/表示
  67.  
  68. “””
  69.  
  70.  
  71. import tkinter
  72. #from tkinter import messagebox
  73. #import numpy as np
  74. #import cv2
  75. import serial
  76. import binascii
  77. #import time
  78. import threading
  79. #from threading import (Event, Thread)
  80.  
  81. #UART_9600                    = b”$PMTK251,9600*17\r\n”                                        # ボーレート 9600bps        ※ACK無し
  82. UART_38400                = b”$PMTK251,38400*27\r\n”                                        # ボーレート 38400bps        ※ACK無し
  83. SET_INTERVAL_300MS    = b”$PMTK220,300*2D\r\n”                                            # 受信情報更新間隔 0.3S    ※38400bps以上で設定可
  84. #SET_INTERVAL_1000MS    = b”$PMTK220,1000*1F\r\n”                                        # 受信情報更新間隔 1S
  85. SBAS_ENABLE            = b”$PMTK313,1*2E\r\n”                                                # SBAS受信_ON
  86. 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センテンスのみ出力
  87.  
  88. serial_dat = 0
  89. RUN = 1
  90. PAUSE = 0
  91. OK = 0
  92. NG =    1
  93.  
  94. # ACK受信処理 ※UART通信レート以外の設定は、ACK応答の比較を行う
  95. def ACK_Receive(num):
  96.     ser.reset_input_buffer()                                # 受信バッファクリア
  97.  
  98.     # 先頭文字”$”を受信するまで 繰り返し受信
  99.     while True:
  100.         serial_dat = ser.read()                            # シリアルデータを1byte受信
  101.         if serial_dat.decode(“UTF-8”) == ‘$’:
  102.             break
  103.  
  104.     serial_dat = ser.readline()                            # ASCII制御コードLF(\n)まで シリアルコードを受信
  105.     print(serial_dat.decode(“UTF-8”))                #debug ACKコードを表示
  106.     if num == 1:
  107.         if serial_dat[0:13] == b”PMTK001,220,3″:    # 期待値のACKコードと比較
  108.             return OK
  109.     elif num == 2:
  110.         if serial_dat[0:13] == b”PMTK001,313,3″: # 期待値のACKコードと比較
  111.             return OK
  112.     elif num == 3:
  113.         if serial_dat[0:13] == b”PMTK001,314,3″: # 期待値のACKコードと比較
  114.             return OK
  115.  
  116.     return NG
  117.  
  118. # 受信モジュールへコマンド送信処理 と ACK応答待ち
  119. def GNSS_command():
  120.     for i in range(4):
  121.         while ser.out_waiting != 0: {}                    # 送信完了待ち
  122.  
  123.         if i == 0:
  124.             ser.write(UART_38400);                        # UART通信レートを設定 ※この設定のACK応答待ちは行わない。(->モジュールからのACK応答が確認できない)
  125.         elif i == 1:
  126.             while ACK_Receive(i) == NG:
  127.                 ser.write(SET_INTERVAL_300MS);    # GNSSデータ転送間隔を設定
  128.                 print(‘SET_INTERVAL ACK(PMTK001,220,3) -> NG’)        #debug
  129.             print(‘SET_INTERVAL ACK(PMTK001,220,3) -> OK’)            #debug
  130.         elif i == 2:
  131.             while ACK_Receive(i) == NG:
  132.                 ser.write(SBAS_ENABLE)                # SBASの受信を許可
  133.                 print(‘SBAS ACK(PMTK001,313,3) -> NG’)                    #debug
  134.             print(‘SBAS ACK(PMTK001,313,3) -> OK’)                        #debug
  135.         elif i == 3:
  136.             while ACK_Receive(i) == NG:
  137.                 ser.write(PMTK_GGA_OUT)            # GPGGAデータのみの出力へ設定
  138.                 print(‘GPGGA_OUT ACK(PMTK001,314,3) -> NG’)    #debug
  139.             print(‘GPGGA_OUT ACK(PMTK001,314,3) -> OK’)        #debug
  140.  
  141. #RUNボタンの定義
  142. def click_Run_button():
  143.     gnss_thread.thread_state = RUN                    #GNSSデータ受信開始
  144.  
  145. # Pauseボタンの定義
  146. def click_Pause_button():
  147.     gnss_thread.thread_state = PAUSE                #GNSSデータ受信中断 ※シリアルポートは開いたままで、受信データは未使用。
  148.  
  149. #GNSSデータの受信 ・・・ スレッド処理
  150. class GNSS_Data_Receive(threading.Thread):
  151.     def __init__(self):
  152.         threading.Thread.__init__(self)
  153.         self.thread_state = PAUSE
  154.  
  155.         # テキストボックスを4つ作成。 シリアルデータとその換算値は、このテキストボックス内に表示される。
  156.         input_label = tkinter.Label(text = “Original “)
  157.         input_label.place(x=1, y=70)
  158.         self.text_gpgga = tkinter.Text(root_window, width=50, height=1)
  159.         self.text_gpgga.place(x = 70, y=70)
  160.  
  161.         input_label = tkinter.Label(text = “TIME “)
  162.         input_label.place(x=1, y=100)
  163.         self.text_time = tkinter.Text(root_window, width=50, height=1)
  164.         self.text_time.place(x = 70, y=100)
  165.  
  166.         input_label = tkinter.Label(text = “North “)
  167.         input_label.place(x=1, y=130)
  168.         self.text_latitude = tkinter.Text(root_window, width=50, height=1)
  169.         self.text_latitude.place(x = 70, y=130)
  170.  
  171.         input_label = tkinter.Label(text = “East”)
  172.         input_label.place(x=1, y=160)
  173.         self.text_longitude = tkinter.Text(root_window, width=50, height=1)
  174.         self.text_longitude.place(x = 70, y=160)
  175.  
  176.     def run(self):
  177.         while True:                                                                    # スレッド処理の繰り返し            ※これが無いと、Waitボタンを押した後、RUNボタンを押しても再開しない。
  178.             while self.thread_state == RUN :
  179.                 ser.reset_input_buffer()                                            # 受信バッファクリア
  180.                 serial_dat = ser.read()                                            # シリアルデータを1byte受信
  181.                 if serial_dat.decode(“UTF-8”) == ‘$’ :                        # 受信した1バイトのデータが、GPGGAデータの先頭文字”$”なら、残りの41バイトを取り込む
  182.                     serial_dat = ser.read(41)                                        # シリアルデータを41byte受信
  183.                     header = serial_dat[0:5]                                        # 先頭5文字(”GPGGA”)を取得
  184.                     serial_dat = serial_dat.decode(“UTF-8”)                # byte型をstr型へ変換
  185.                     header = header.decode(“UTF-8”)                        # byte型をstr型へ変換
  186.  
  187.                     # GPSモジュールが出力するオリジナルデータの表示
  188.                     self.text_gpgga.delete(“1.0”, “end”)                        # 対象行の表示削除
  189.                     self.text_gpgga.insert(tkinter.END, str(serial_dat))    # GPGGAデータの表示 ※ ”$”は非表示、42バイト目移行も非表示
  190.  
  191.                     # 時刻のバイトデータを数値(int型)データへ変換し、表示/更新
  192.                     time_hour = int(serial_dat[6:8]) + 9                        # 世界標準時(UTC)を 日本標準時(JST)へ補正 (+9時間)                
  193.                     if time_hour >= 24:
  194.                         time_hour -= 24
  195.                     self.text_time.delete(“1.0”, “end”)                        # 対象行の表示削除
  196.                     self.text_time.insert(tkinter.END, str(time_hour)+” : ” + serial_dat[8:10] + ” : ” + serial_dat[10:12])    # 時刻の表示
  197.  
  198.  
  199.                     #緯度のバイトデータを数値(int型)データへ変換し、表示/更新 ※21byte目の小数点を削除。 NMEA表記(xx度yy.zz分)を10進表記(xx.aaa度)に変換
  200.                     latitude_upper = int(serial_dat[17:19])
  201.                     latitude_lower = (int(serial_dat[19:21])*10000 + int(serial_dat[22:26]))/600000
  202.                     latitude = latitude_upper + latitude_lower
  203.                     #print(str(latitude)) # debug
  204.                     self.text_latitude.delete(“1.0”, “end”)                        # 対象行の表示削除
  205.                     self.text_latitude.insert(tkinter.END, str(latitude))        # 北緯の表示
  206.  
  207.                     #経度のバイトデータを数値(int型)データへ変換し、表示/更新 ※34byte目の小数点を削除。 NMEA表記(xx度yy.zz分)を10進表記(xx.aaa度)に変換
  208.                     longitude_upper = int(serial_dat[29:32])
  209.                     longitude_lower = (int(serial_dat[32:34])*10000 + int(serial_dat[35:39]))/600000
  210.                     longitude = longitude_upper + longitude_lower
  211.                     #print(str(longitude)) # debug
  212.                     self.text_longitude.delete(“1.0”, “end”)                        # 対象行の表示削除
  213.                     self.text_longitude.insert(tkinter.END, str(longitude))    # 東経の表示
  214.  
  215.                     “”” tkinter.Label(と.placeの組み合わせ)を(繰り返し)使うと、10分以内に表示が崩れるので、以下の処理は未使用。 (5分でカクつく、10分以内に表示が崩れる)
  216.                     #時刻、緯度と経度をウィンドウへ表示
  217.                     input_label = tkinter.Label(text = ” “, font = 16)                                # 一度文字を消す
  218.                     input_label.place(x=1, y=110)
  219.                     input_label = tkinter.Label(text = “TIME ” + str(time_hour) + ” : ” + serial_dat[8:10] + ” : ” + serial_dat[10:12] , font = 16)
  220.                     input_label.place(x=1, y=110)
  221.  
  222.                     input_label = tkinter.Label(text = ” “, font = 16)                                # 一度文字を消す
  223.                     input_label.place(x=1, y=150)
  224.                     input_label = tkinter.Label(text = “北緯 ” + str(latitude) , font = 16)
  225.                     input_label.place(x=1, y=150)
  226.  
  227.                     input_label = tkinter.Label(text = ” “, font = 16)                                # 一度文字を消す
  228.                     input_label.place(x=1, y=190)
  229.                     input_label = tkinter.Label(text = “東経 ” + str(longitude) , font = 16)
  230.                     input_label.place(x=1, y=190)
  231.                     “””
  232.  
  233. #######################################################
  234. ################### ここからメイン #########################
  235.  
  236. # シリアルポートの定義とオープン
  237. ser = serial.Serial(
  238.                         port = ‘COM4’,
  239.                         baudrate = 38400,
  240.                         parity = serial.PARITY_NONE,
  241.                         bytesize = serial.EIGHTBITS,
  242.                         stopbits = serial.STOPBITS_ONE,
  243.                         timeout = None,
  244.                         xonxoff = 0,
  245.                         rtscts = 0,
  246. )
  247.  
  248. #シリアルポートのバッファクリア
  249. ser.reset_input_buffer()                                            # 受信バッファクリア
  250. ser.reset_output_buffer()                                        # 送信バッファクリア
  251.  
  252. # GNSSモジュールの設定
  253. while     GNSS_command() == NG:    {}
  254.  
  255. # 表示ウィンドウの作成
  256. root_window = tkinter.Tk()
  257. root_window.title(“GNSS GPAGGA”)
  258. root_window.geometry(“500×240”)
  259.  
  260. #ボタンの作成 2ヶ
  261. Run_button0=tkinter.Button(root_window, width=5, text=”Run”, command=lambda:click_Run_button())        #シンプル表示
  262. #Run_button0=tkinter.Button(root_window, width=5, background=”#0000ff”, fg = ‘#ffffff’, text=”Run”, command=lambda:click_Run_button())
  263. Run_button0.place(x=30, y=10)
  264. Pause_button0=tkinter.Button(root_window, width=5, text=”Wait”, command=lambda:click_Pause_button())        #シンプル表示
  265. #Pause_button0=tkinter.Button(root_window, width=5, background=”#ff0000″, fg = ‘#000000’, text=”Wait”, command=lambda:click_Pause_button())
  266. Pause_button0.place(x=100, y=10)
  267.  
  268. #スレッド処理で、GNSS_Data_Receive関数を並行して実行
  269. gnss_thread = GNSS_Data_Receive()
  270. gnss_thread.start()                                                #スレッド処理開始
  271.  
  272. # 繰り返し実行
  273. root_window.mainloop()
  274.  
  275. ser.close()
  276. #cap.release()
  277. #cv2.destroyAllWindows()
  278.  
  279.  
  280. “””
  281. 開発履歴 (やってみたことの概要)
  282. <1>
  283.  メイン: メインウインドウの生成、ボタンの生成
  284.  スレッド: シリアルデータ受信、ラベルの表示/更新
  285.  結果: 5分でもたつく。10分以内に、表示が崩れる。
  286.  検討: ラベルで表示する代わりに、コマンド画面に表示させた場合、10分経過でももたつき無し。 このことから、シリアル受信処理が原因では無い様子。
  287.  
  288. <2>    ・・・ 概要 : <1>をベースに、表示の更新頻度を下げてみた。— 小変更
  289.  メイン: メインウインドウの生成、ボタンの生成
  290.  スレッド: シリアルデータ受信、ラベルの表示/更新。 シリアル出力周期を最大限下げ(300mS→1000mS)、ラベルの更新頻度を下げてみた。
  291.  結果: 10分以内にもたつき始める。15分程度で、表示が崩れる。
  292.  検討: まだ、ラベルの表示/更新処理の負荷が高い状態。
  293.  
  294.  
  295. <3> ・・・ 概要 : <1>をベースに、メインとスレッドで担当する処理を入れ替えた。— 大変更
  296.  メイン: シリアルデータ受信、ラベルの表示/更新
  297.  スレッド: メインウインドウの生成、ボタンの生成
  298.  結果: 5分でもたつく。10分以内に、表示が崩れる。 -> <1>と同じ
  299.             メインもスレッドも処理能力は、同程度と判断。
  300.             更新データの表示方法の再検討が必要。
  301.  検討:
  302.   Tk.Labelに関して調査/検索した結果、この処理は負荷が高く、頻繁に繰り返すと処理が追いつかなくなる
  303.     様なので、Tk.Textでシリアルデータを表示する方式に切り換えてみる。
  304.     → 但し、tk.text の方が軽処理である根拠はない、が、
  305.       ラベルは、タイトル文字の様に、更新しない文字を表示するためのもので、
  306.       text(テキストボックス)は、ユーザーの打ち込む文字など、頻繁な更新と即時反映を想定した処理になっているかも・・・と考えた。
  307.  
  308. <4> ・・・ 概要 : <1>をベースに、テキストボックスに表示する処理へ変更した。— 中変更
  309.  メイン: メインウインドウの生成、ボタンの生成
  310.  スレッド: シリアルデータ受信、テキストボックスの表示/更新 ※シリアル出力周期は、0.3秒
  311.  結果: 40分経過後も、カクつき/表示異常無し なので、<4>のテキストボックス表示方式を採用
  312.  
  313. “””

Hardware & Software
スポンサーリンク
シェアする
MTNブログ

コメント