01.13a GPS ~NMEA-0183 解析編 1~

一応前回のサンプルプログラムでGPS衛星のデータは正常に受信出来て、その内容から緯度経度を抜き出せそうだということは分かりました。今回はpythonを使った実践編として慣れるという意味でも呆気なく出来てしまうのも何なんで勉強も兼ねて自分で解析し緯度経度以外にも使用できそうなものがあったら、どんどん利用していきたいと考えています。それに出来ない場合は前回のサンプルを使用すれば少なくとも緯度経度は求まるという安心感がありますからね(一応手探りでやっているので)。

① 何が送られてきているのか?

NMEA-0183Aは最初に”$”、最後<CR><LF>で終わり、1つ前にはチェックサムの構成となっています。「チェックサム」が何であるかは次のセクションでお話しするとして、この転送データは”$”以降どんなデータが続くのか最初に調査したいと思います。

”$”続きに文字列、以降データと続きますが、最初の文字列の構成は

   $ GP GGA
     |    |        |
     |    |        +→ 時刻、緯度経度、標高、測位状態、DGPS基地局番号などの基本情報
     |    +→ トーカ ID  GP はGPS機器, GL はGLONASS
     +→メッセージ開始文字

のようになっているとウィキペディアに書いてあり、うち「GLONASS」はウィキペディアによると

『ソビエト連邦が開発し、現在はロシア宇宙軍の手によってロシア政府のために運用されている衛星測位システムである。アメリカ合衆国によって運用されているグローバル・ポジショニング・システム(GPS)や、欧州連合(EU)によって計画されているガリレオなどに対応した、ロシアの衛星測位システムである。』

とあります。その内使われる可能性がありそうなのは、

    "$GPGGA"・"$GPGLL"・"$GPGSA"・"$GPGSV"
    "$GPRMC"・"$GPVTG"・"$GPZDA"


上記の7ケースではないかと思われます。

そこで実際10000件のデータを読んでみて、7ケースの使用頻度を調べてみることにしました。


gps_03・py
#coding: utf-8
# //////////////////////////////////////////////////////////
# //  【改訂履歴】                                        //
# // V0.00 2019/12/06 プロトタイプ                           //
# //////////////////////////////////////////////////////////
import sys
import serial
import serial.tools.list_ports


# //////////////////////////////////////////////////////////
# /// シリアルポートの検索                                   //
# //////////////////////////////////////////////////////////
def search_com_port():
    coms = serial.tools.list_ports.comports()
    list_com = []
    for com in coms:
        list_com.append(com.device)
    print('Connected COM ports: ' + str(list_com))
    used_port = list_com[0]
    print('Use COM port: ' + used_port)

    return used_port


# //////////////////////////////////////////////////////////
# /// gps_03 NMEA 0183 分析                              ///
# //////////////////////////////////////////////////////////
def gps_03_PROC(self):
    print("【gps_03_PROC】-->\n")

    NMEA_0183 = {
    "$GPGGA" : 0,
    "$GPGLL" : 0,
    "$GPGSA" : 0,
    "$GPGSV" : 0,
    "$GPRMC" : 0,
    "$GPVTG" : 0,
    "$GPZDA" : 0,
    }

    used_port = search_com_port()

    ser = serial.Serial(used_port , 115200,timeout=None)
    cnt = 0
    while True:
        sentence = ser.readline().decode('utf-8')
        list_text = sentence.split(",")

        cnt += 1
        if  cnt >= 10000:
            break
        if  (cnt % 1000) == 0:
            print(cnt / 1000)


        if sentence[0] != '$': # 先頭が'$'でなければ捨てる
            continue
        else:
            NMEA_0183[list_text[0]] += 1
    ser.close()

    print(NMEA_0183)
    print("\n-->【finish】")

★ 謎のエラー

実行結果は「例外処理」で異常終了となってしまいました。

またいつものpythonの「はまりポイント」出現かと思い、ウィペディアには載っていない「未知のケース」が出たら例外処理として、未知の行全てを表示できるように改修してみました。


gps_03・py
#coding: utf-8
# //////////////////////////////////////////////////////////
# //  【改訂履歴】                                        //
# // V0.00 2019/12/06 プロトタイプ                           //
# //////////////////////////////////////////////////////////
import sys
import serial
import serial.tools.list_ports


# //////////////////////////////////////////////////////////
# /// シリアルポートの検索                                   //
# //////////////////////////////////////////////////////////
def search_com_port():
    coms = serial.tools.list_ports.comports()
    list_com = []
    for com in coms:
        list_com.append(com.device)
    print('Connected COM ports: ' + str(list_com))
    used_port = list_com[0]
    print('Use COM port: ' + used_port)

    return used_port


# //////////////////////////////////////////////////////////
# /// gps_03 NMEA 0183 分析                              ///
# //////////////////////////////////////////////////////////
def gps_03_PROC(self):
    print("【gps_03_PROC】-->\n")
    NMEA_0183 = {
    "$GPGGA" : 0,
    "$GPGLL" : 0,
    "$GPGSA" : 0,
    "$GPGSV" : 0,
    "$GPRMC" : 0,
    "$GPVTG" : 0,
    "$GPZDA" : 0,
    }
    # $PSRF156,23,1,0*09


    # Search COM Ports
    used_port = search_com_port()

    ser = serial.Serial(used_port , 115200,timeout=None)
    cnt = 0
    while True:
        sentence = ser.readline().decode('utf-8')
        #print(sentence,end="")
        list_text = sentence.split(",")

        cnt += 1
        if  cnt >= 10000:
            break
        if  (cnt % 1000) == 0:
            print(cnt / 1000)


        if sentence[0] != '$': # 先頭が'$'でなければ捨てる
            continue
        else:
            try:
                if  (list_text[0] in NMEA_0183):
                    NMEA_0183[list_text[0]] += 1
                else:
                    NMEA_0183[list_text[0]] = 1
                    print(list_text[0])
            except Exception as e:
                print('->> gps_03 error 1' , e , sentence)

    ser.close()
    try:
        print(NMEA_0183)
    except Exception as e:
        print('->> gps_03 error 2',e)
    print("\n-->【finish】")

この結果エラー時には、

    $PSRF156,23,1,0*09

という短文のメッセージが載っていることが分かりました。
一応手計算でチェックサム処理をすると正常値なので衛星がだしているのではないかと思われます。


その他回数に関する結果は

    $GPGGA    2777
    $GPGLL    :0
    $GPGSA    :2776
    $GPGSV    166
    $GPRMC    2776
    $GPVTG     0
    $GPZDA     0
    $PSRF156   5

であることが判明しました。




★ なぜのエラーの正体

皆さんは「インディペンデンス・デイ」という映画を観たことありますか?

巨大円盤とともにエイリアンが攻めてくるのですが、最初は放送衛星に紛れて異様なパルス信号を組み込んでいたという話から始まるのですが、まさに、これ「規格にない信号」を見つけました。

なんかワクワクしませんか?



そこでネット検索していると、下記米国情報を入手しました。

    「NMEA_Reference_Manual_(CS-129435-MA-1)」
https://www.inventeksys.com/wp-content/uploads/2012/05/NMEA-Reference-Manual-CS-129435-MA-2.pdf#search=%27nmea0183+PSRF156+23%27


日本語訳は見つかりませんでしたが、これは「NMEA」のリファレンスマニュアルで、分かる範囲で見ていくと、

ウィキペディアには アプリケーションレイヤプロトコル規格

・ それぞれのメッセージの開始文字はドル記号。
・ 次の5文字はトーカ(2文字)とメッセージ(3文字)を識別。
・ 全ての後続するデータフィールドはコンマ区切り。

のように載っており他のサイトプロトコル解説にも同様の説明ですが、実際には違うようです。


使用される用語も含め正確に解析すると、EMEAプロトコルは、

    「メッセージID」+「データ」+「*チェックサム」+<CR><LF>

で構成され値ます。

メッセージID」構成は

 「$」 + 「メッセージID」 + 「プロトコルヘッダー」
          または
 「$」 + 「メッセージID」 + 「プロトコルヘッダー」 + 「、サブコード(0x**)」

によって、構成されています。

「メッセージID」 の内、識別信号分類は下記、

識別番号 トーカ(装置名)
AG オートパイロット(一般)
AP オートパイロット(磁気)
AI AIS
CD 通信装置(DSC)
CR 通信装置(データ受信機)
CS 通信装置(衛星)
CT 通信装置(MF/HF)
CV 通信装置(VHF)
CX 通信装置(走査受信機)
DE DECCA
DF 方探
EC 電子海図装置(ECS)
EI ECDIS
EP EPIRB
ER エンジンモニタ
GP GPS
GL GLONASS受信機
GN GNSS
HC マグネットコンパス
HE ジャイロ(Northseeking)
HN ジャイロ(Non-north seeking)
II 複合装置
IN 複合航法装置
LC ロランC
RA レーダー
SD 測深器
SN 電子測位装置(一般)
SS スキャニングソナー
TI ターンレート表示器
VD ドップラーログ
VM スピードログ(対水)
VW 機械式ログ(対水)
VR 航海記録器
YX 振動子
ZA 原子時計
ZC クロノメーター
ZQ 水晶時計
ZV 無線修正時計
WI 気象計器

しかしメッセージIDは識別信号だけではなく、おそらくGPS衛星の制御用メッセージもあり、IDはに文字ですが制御用メッセージIDは4文字 「PSRF」であると思われます。

構成はも「プロトコルヘッダー」+「サブコード」が一体で「プロトコルヘッダー」という扱いとなっており、、一覧にすると下記のようになります。

従って「謎のコード」の正体はGPS衛星の制御信号でメッセージを解読すると下記となります。

    ECLM 開始要求をダウンロードします

※ECLMはEmbedded ClientLocation Manager (ECLM)  暦をサポートするファームウエアの呼称


Message Description
GGA Time,・position and fix type data
GLL Latitude,・longitude,・UTC time of position fix and status
GSA GPS receiver operating mode,・satellites used in the position solution,・and DOP values
GSV Number of GPS satellites in view satellite ID numbers,・elevation,・azimuth,・& SNR values
MSS Signal-to-noise ratio,・signal strength,・frequency,・and bit rate from a radio-beacon receiver
RMC Time,・date,・position,・course and speed data
VTG Course and speed information relative to the ground
ZDA PPS timing message (synchronized to PPS)
150 OK to send message
151 GPS Data and Extended Ephemeris Mask
152 Extended Ephemeris Integrity
154 Extended Ephemeris ACK
155 Extended Ephemeris Proprietary Message
156,0x20 ECLM ACK/NACK
156,0x21 ECLM EE Get Age response
156,0x22 ECLM Get SGEE Age response
156,0x23 ECLM Download Initiate Request
156,0x24 ECLM Erase Storage File
156,0x25 ECLM Update File Content
156,0x26 ECLM Request File Content
160 Watchdog Timeout and Exception Condition




★ サムチェックについて

次に据え置きした「サムチェック」についてお話しします。

以前シリアル通信を小包の例えをしましたが、

このお話でいうと、英国から送られてきた小包がやっと手元に届き伝票と照らし合わせて中身を確認したところから始まります。

機械の場合日本語でいうと「自己診断プログラム」=「セルフチェック」というのを行って正しく動くか確認します。このセルチェック機能の材料が「サムチェック」にあたります。

送りては送る前にセルフチェックさせてデータを「サムチェック」という個所に残し、受け取り手は同じセルフチェックさせたときの結果と「サムチェック」データとを照合することで一致すれば正常と判断されるわけです。

しかし 「サムチェック」といっても色々な種類があって、GGEが知っているだけでも

 1) 積算方式

 2) 排他的論理和方式


例えば今回送られてきた不審な贈り物のチェックサムを手動で行ってみます。 伝文は、

    $PSRF156,23,1,0*09

これを16進数のASCIIコードで読み換えると(表示上先頭に16進数の意味で「0x」を添付します)

  $      P     S     R     F      1     5      6      , 
  0x24 0x50 0x53 0x52 0x46 0x31 0x35 0x36 0x2C
  2      3     ,       1      ,       0      *      0     9      <CR><LF>
  0x32 0x33 0x2C 0x31 0x2C 0x30 0x2A 0x30 0x39 0x0D 0x0A

のようになります。

可能性がある1バイトの変数に排他的論理和(XOR)する場合と、加算していく場合計算します。
実際にプログラムを作成し、どちらのケースをやっているのか確認しましょう。

sample・py
#coding: utf-8
import sys,os
sys.path.append("c:\\open_jtalk\\bin\\")

NMEA_0183 = "$PSRF156,23,1,0*09"

size = len(NMEA_0183)
sumXor = 0x24
sumAdd = 0
for cd in NMEA_0183:
    if  size <= 3:
        break
    else:
        size -= 1

    sumXor = sumXor ^ ord(cd)
    sumAdd = sumAdd + ord(cd)
    print(cd , hex(ord(cd)) , hex(sumXor) , hex(sumAdd % 256))

結果は下記のようになりました。
(nnabla) C:\work>python sample.py
$ 0x24 0x0 0x24
P 0x50 0x50 0x74
S 0x53 0x3 0xc7
R 0x52 0x51 0x19
F 0x46 0x17 0x5f
1 0x31 0x26 0x90
5 0x35 0x13 0xc5
6 0x36 0x25 0xfb
, 0x2c 0x9 0x27
2 0x32 0x3b 0x59
3 0x33 0x8 0x8c
, 0x2c 0x24 0xb8
1 0x31 0x15 0xe9
, 0x2c 0x39 0x15
0 0x30 0x9 0x45

ここから分かることは、

1)チェックサムのセパレータは「*」

2)一見すると二文字
チェックサム用セパレータと<CR><LF>間は2文字=2バイトのように感じますが、上記結果の3列目に表示されている結果と同じであることから16進数で書かれていることが分かり2文字で1バイトの16進数であることが分かります。

3)チェックサムは排他的論理和方式を採用
同じく結果が4列目ではなく3列目であることから「排他的論理和(XOR)方式チェックサム」であることが判明しました。


例えばイーサネット・USB間通信など最新の通信システムではユーザが通信伝文をいちいちチェックするという面倒なことを行うプログラムはあまりありません。なぜならそれはイーサネット通信はデータがと元に届くまでの間にデータの中身等をチェックするレイヤを独自に持っていて破損している場合はリトライして正しい伝文が届くま行うか出来ない場合はエラーメッセージで知らせてくれる機能が既に備わっているからです。

でもこの原始的シリアル通信は、そのような機能はなくデータの保証は自分で行わないといけないのです。実際対象が衛星なので天候・建物の中などの遮蔽エラーにより伝文破壊は起こりうるため必ず実施する必要性があります。1行に一回必ず同じルールで行われるため受信処理内で共通して行う処理をつくることで共通化を図るとともに、一回作れば終了となります。



★ サムチェックプログラム組み込み

実際、受信電文チェック関数を組み込んでみます。

シリアルポート検索とチェックサムチェック処理は、後日ユーティリティーモジュールに組み込むことを想定し独自関数にして動作させます。


今回 前述の単体テストと少々違った点があります。

【注意点】

① 目に見えない文字に注意
コメントにもあるようにラインインプット収集した文字列にはキャリッジリターンの<cr><lf>を含んでいます。このため目に見える文字列だけを対象にしがちですが、データサイズやチェックサム位置を勘定する際のインデックス番号にちゅいしてください。


② 16進数の整数表記
チェックサムは2文字の16進数で出来ているので16進数文字配列→整数値変換を行わないと解けません。このためVC++ではあまり見かけない「int()のオプション付き」で行います。
int(x,16)は16進数
であることを意味しています。

またこの場合の文字列は「0x**」の形ではなく「**」で中身だけ16進数であることにも注意してください。



gps_03・py
#coding: utf-8
# //////////////////////////////////////////////////////////
# //  【改訂履歴】                                        //
# // V0.00 2019/12/06 プロトタイプ                           //
# //////////////////////////////////////////////////////////
import sys
import serial
import serial.tools.list_ports


# //////////////////////////////////////////////////////////
# /// シリアルポートの検索                                   //
# //////////////////////////////////////////////////////////
def search_com_port():
    coms = serial.tools.list_ports.comports()
    list_com = []
    for com in coms:
        list_com.append(com.device)
    print('Connected COM ports: ' + str(list_com))
    used_port = list_com[0]
    print('Use COM port: ' + used_port)

    return used_port


# /////////////////////////////////////////////////////////
# /// シリアルデータチェック                                //
# /////////////////////////////////////////////////////////
def serial_data_check( sentence ):
    if sentence[0] != '$':
        return False

    size = len(sentence)
    sumXor = 0x24

    for cd in sentence:
        # readlineの場合"*00"<cr><lf> → 5
        if  size <= 5:
            break
        else:
            size -= 1

        sumXor = sumXor ^ ord(cd)

    # readlineの場合"*00"<cr><lf> → -4 ~ -2
    sum = sentence[(len(sentence) - 4):(len(sentence) - 2)]
    #             ↓ int(A,para) paraはAが16進数の場合
    if  int(sum , 16) == sumXor:
        return True
    else:
        return False



# /////////////////////////////////////////////////////////
# /// gps_03 NMEA 0183 分析                             ///
# /////////////////////////////////////////////////////////
def gps_03_PROC(self):
    print("【gps_03_PROC】-->\n")

    NMEA_0183 = {
    "$GPGGA" : 0,
    "$GPGLL" : 0,
    "$GPGSA" : 0,
    "$GPGSV" : 0,
    "$GPRMC" : 0,
    "$GPVTG" : 0,
    "$GPZDA" : 0,
    }
    # $PSRF156,23,1,0*09


    # Search COM Ports
    used_port = search_com_port()

    ser = serial.Serial(used_port , 115200,timeout=None)
    cnt = 0
    while True:
        sentence = ser.readline().decode('utf-8')
        #print(sentence,end="")
        list_text = sentence.split(",")

        cnt += 1
        if  cnt >= 10000:
            break
        if  (cnt % 1000) == 0:
            print(cnt / 1000)

        if serial_data_check( sentence ) == False:
            print('->> gps_03 invaride' , sentence)
            continue
        else:
            try:
                if  (list_text[0] in NMEA_0183):
                    NMEA_0183[list_text[0]] += 1
                else:
                    NMEA_0183[list_text[0]] = 1
                    print(list_text[0])
            except Exception as e:
                print('->> gps_03 error 1' , e , sentence)

    ser.close()
    try:
        print(NMEA_0183)
    except Exception as e:
        print('->> gps_03 error 2',e)
    print("\n-->【finish】")



【参考URL】
https://www.tokyo2show.co.jp/knowledge/?p=17
NMEA-0183 プロトコル 更新中

https://ja.wikipedia.org/wiki/NMEA_0183
NMEA 0183

https://ja.wikipedia.org/wiki/GLONASS
GLONASS

https://www.hiramine.com/physicalcomputing/general/gps_nmeaformat.html
GPSのNMEAフォーマット

https://note.nkmk.me/python-str-extract/
Pythonで文字列を抽出(位置・文字数、正規表現)

https://ambidata.io/blog/2017/08/02/gps/
Raspberry Pi3のPythonでGPSを扱う

https://piyajk.com/archives/302
NMEA 0183 sentences データ解析

https://github.com/inmcm/micropyGPS
inmcm/micropyGPS

https://qiita.com/grinpeaceman/items/23af7733962268bd900c
[Python]pyorbitalで簡単衛星軌道予測

https://celestrak.com/
CelesTrak

https://apod.nasa.gov/apod/ap001127.html
世界地図



≪メインフレーム・目次が表示されない場合はここをクリックしてください≫ Copyright(c)2018 GGE Kiyosu Cyber Club Allrights Reserved