NMEA-0183メッセージの解析
GPSモジュールが出力するNMEA-0183メッセージと呼ばれる出力文字列の解析プログラムの実験を行いました。最終的にはSTM32 Primer2に組み込んで使用するのですが、今回はPC上(VC++ 2008)で動作確認を行いました。
NMEA-0183メッセージとは
GPSモジュールが衛星を補足すると、以下のような文字列をシリアルインターフェースから吐き出します(自宅で収集したデーターのため、緯度・経度を示す部分は、xxxx.xxxx, yyyy.yyyyに書き換えています)。
$GPGGA,230400.600,xxxx.xxxx,N,yyyy.yyyy,E,1,4,39.03,-170.6,M,39.4,M,,*4D $GPGLL,xxxx.xxxx,N,yyyy.yyyy,E,230400.600,A,A*56 $GPGSA,A,3,06,03,31,19,,,,,,,,,46.41,39.03,25.10*35 $GPGSV,3,1,10,16,64,350,,06,56,223,29,31,56,129,26,03,42,229,30*74 $GPGSV,3,2,10,23,34,287,,21,31,086,,19,17,218,26,13,14,320,*7A $GPGSV,3,3,10,29,08,043,,25,04,317,*7A $GPRMC,230400.600,A,xxxx.xxxx,N,yyyy.yyyy,E,0.05,169.01,131109,,,A*60 $GPVTG,169.01,T,,M,0.05,N,0.08,K,A*3F
各パラメーターの意味は、このリンクを参照ください。概略を示すと;
- GPGGAメッセージから、衛星の補足状態、経度・緯度や補足している衛星の数が分かります
- GPGSVメッセージから、個々の衛星の位置(仰角・方位)、受信信号レベルが分かります
- 経度・緯度情報は複数のメッセージに挿入されており、GPGLL, GPRMCメッセージでも取得できます
NMEA-0183メッセージの解析
経度・緯度を得るためには、GPGGAの「GPSのクオリティ(Position Fix Indicator)」が1 or 2になった後で、経度・緯度情報を取り出す必要があります。NMEA-0183メッセージ(以下NMEAメッセージ)は、カンマ区切り形式の単純な構造であるため、解析はそれほど難しくないと思うのですが、チェックサムの計算とか色々考慮すべき点もあります。また、経度・緯度情報だけ抜き取るのなら簡単ですが、汎用性を持たせて任意のデーターの取り出しまで求めるとプログラムは結構複雑そうです。
そこで、例によって人様の成果を活用させてもらうべく、フリーのnmeaパーサーを探しました。グーグル様で検索してみるとC言語で使用できるライブラリとしては、[dmh2000] – NMEAP, NMEA Libraryが見つかりました。NMEAPはGPGGA/GPRMCのみを解析しているのに対して、NMEA LibraryはすべてのNMEAメッセージを解析できます。GPGSVメッセージも解析して衛星からの信号強度も表示したいため、NMEA Libraryを使用することにします。
NMEA Libraryの使い方
パッケージに含まれるexampleをベースに以下のサンプルプログラムを書いてみました(ほとんどexampleそのままです、、)。サンプルプログラムはテキスト形式で保存したNMEAメッセージのログファイルを読み込んで、位置情報と信号強度を表示します。
#include "nmea/nmea.h" #include <string.h> #include <stdio.h> #ifdef NMEA_WIN # include <io.h> #endif void trace(const char *str, int str_size) // debug用のcallback { printf("Trace: "); write(1, str, str_size); } void error(const char *str, int str_size) { printf("Error: "); write(1, str, str_size); } int main(int argc, char *argv[]) { nmeaINFO info; // 取得情報を保持するための構造体 nmeaPARSER parser; // パーサーの内部管理情報 FILE *file; char buff[256]; // 受信データーのバッファ int size, it = 0, i; if (argc < 2) { printf("need log filename"); return -1; } file = fopen(argv[1], "rb"); if(!file) { printf("File open error"); return -1; } nmea_property()->trace_func = &trace; // trace callbackの登録 nmea_property()->error_func = &error; nmea_zero_INFO(&info); // nmeaINFO構造体の初期値設定 nmea_parser_init(&parser); // パーサーオブジェクトの初期化 while(!feof(file)) { size = (int)fread(&buff[0], 1, 128, file); // NMEAメッセージの取得 nmea_parse(&parser, buff, size, &info); // パーサーの起動 printf( // 経度・緯度情報の表示 "%03d, Lat: %f, Lon: %f, Sig: %d, Fix: %d\n", it++, info.lat, info.lon, info.sig, info.fix ); for (i = 0; i < NMEA_MAXSAT; i++) { // 受信信号強度の表示 if (info.satinfo.sat[i].in_use) printf( " sat_id:%02d, sig:%02d\n", info.satinfo.sat[i].id , info.satinfo.sat[i].sig); } } fseek(file, 0, SEEK_SET); nmea_parser_destroy(&parser); // パーサーオブジェクトの廃棄(メモリーの開放など) fclose(file); return 0; }
10, 15行目は、デバック用のcallback関数で、43, 44行目で登録しています。ソースを眺めてみたのですが、どのタイミングで呼び出しているかは分からず。
51, 52行目で、128文字単位にファイルからテキストを読み出してパーサーに渡しています。すなわち、メッセージの切れ目(改行)を意識する必要がないということになります。実際にGPSモジュールをつないで動かした場合、割り込みで文字を受信して一定の文字数がたまったらパーサーを起動するようにすればよく、処理が簡略化できそうです。
解析した情報は、nmeaINFO構造体に格納されます。構造体の定義は以下となっています。
typedef struct _nmeaINFO { int smask; /**< Mask specifying types of packages from which data have been obtained */ nmeaTIME utc; /**< UTC of position */ int sig; /**< GPS quality indicator (0 = Invalid; 1 = Fix; 2 = Differential, 3 = Sensitive) */ int fix; /**< Operating mode, used for navigation (1 = Fix not available; 2 = 2D; 3 = 3D) */ double PDOP; /**< Position Dilution Of Precision */ double HDOP; /**< Horizontal Dilution Of Precision */ double VDOP; /**< Vertical Dilution Of Precision */ double lat; /**< Latitude in NDEG - +/-[degree][min].[sec/60] */ double lon; /**< Longitude in NDEG - +/-[degree][min].[sec/60] */ double elv; /**< Antenna altitude above/below mean sea level (geoid) in meters */ double speed; /**< Speed over the ground in kilometers/hour */ double direction; /**< Track angle in degrees True */ double declination; /**< Magnetic variation degrees (Easterly var. subtracts from true course) */ nmeaSATINFO satinfo; /**< Satellites information */ } nmeaINFO; typedef struct _nmeaSATINFO { int inuse; /**< Number of satellites in use (not those in view) */ int inview; /**< Total number of satellites in view */ nmeaSATELLITE sat[NMEA_MAXSAT]; /**< Satellites information */ } nmeaSATINFO; typedef struct _nmeaSATELLITE { int id; /**< Satellite PRN number */ int in_use; /**< Used in position fix */ int elv; /**< Elevation in degrees, 90 maximum */ int azimuth; /**< Azimuth, degrees from true north, 000 to 359 */ int sig; /**< Signal, 00-99 dB */ } nmeaSATELLITE;
構造体が階層化されており、ちょっとややこしいですが;
- 経度緯度は、info.lat, info.lonで取得できます(infoはnmeaINFOのインスタンス)
- 信号強度は階層をたぐって、info.satinfo.sat[i].sigで取得できます
サンプルを動かした結果を以下に示します(経度・緯度はxxxx.xxxx, yyyy.yyyyで、、)。
パーサーには128文字単位でデーターを渡していますが、パーサー内部でメッセージ単位に処理を行う作りになっています。そのため、最初のGPGSAメッセージが読み込まれた時点で(最初のTrace表示)000番目の解析結果が表示され、続けてGPGSAとGPGSVが読み込まれた時点で001番目の解析結果が表示されています。
Trace: $GPGGA,112645.000,xxxx.xxxxx,N,yyyy.yyyy,E,2,05,1.7,465.4,M,39.4,M,3.8,0000*7C
000, Lat: xxxx.xxxxx, Lon: yyyy.yyyy, Sig: 2, Fix: 1
Trace: $GPGSA,A,3,12,24,21,09,18,,,,,,,,2.0,1.7,1.0*31
Trace: $GPGSV,3,1,12,09,80,345,22,27,71,022,17,32,68,311,,18,62,305,16*7E
001, Lat: xxxx.xxxxx, Lon: yyyy.yyyy, Sig: 2, Fix: 3
Trace: $GPGSV,3,2,12,15,42,071,,12,29,161,39,21,28,241,31,26,26,292,*7E
Trace: $GPGSV,3,3,12,22,26,314,17,24,15,198,37,30,09,186,21,05,06,142,*7A
002, Lat: xxxx.xxxx, Lon: yyyy.yyyy, Sig: 2, Fix: 3
Trace: $GPRMC,112645.000,A,xxxx.xxxx,N,yyyy.yyyy,E,0.00,,170110,,,D*7B
Trace: $GPVTG,,T,,M,0.00,N,0.0,K,D*16
Trace: $GPGGA,112646.000,xxxx.xxxx,N,yyyy.yyyy,E,2,05,1.7,465.4,M,39.4,M,4.8,0000*78
003, Lat: xxxx.xxxx, Lon: yyyy.yyyy, Sig: 2, Fix: 3
Trace: $GPGSA,A,3,12,24,21,09,18,,,,,,,,2.0,1.7,1.0*31
Trace: $GPGSV,3,1,12,09,80,347,22,27,70,022,16,32,69,311,,18,62,305,16*7D
004, Lat: xxxx.xxxx, Lon: yyyy.yyyy, Sig: 2, Fix: 3
sat_id:09, sig:22
sat_id:18, sig:16
sat_id:12, sig:39
sat_id:21, sig:31
sat_id:24, sig:37
Trace: $GPGSV,3,2,12,15,42,072,,12,29,160,39,21,28,240,31,26,26,293,*7C
Trace: $GPGSV,3,3,12,22,26,314,17,24,14,197,37,30,09,186,21,05,06,142,*74
005, Lat: xxxxx.xxxxx, Lon: yyyyy.yyyyy, Sig: 2, Fix: 3
sat_id:09, sig:22
sat_id:18, sig:16
sat_id:12, sig:39
sat_id:21, sig:31
sat_id:24, sig:37
余談
パーサーが動くようになったので、Primer2につないでCircleOS配下でNMEA Libraryを動かそうと作業を始めたのですが、ここで問題発生。Primer2のVBATとGを、GPSモジュールのGとVCCに接続していまい(電源をショートさせてしまい)、GPSモジュールがあえなく昇天してしまいました。SparkFunから直接買ったモジュールで、送料込みで$90以上かかったのに涙です・・・ 電源周りの接続ミスで部品を壊したのは2度目。まったく注意散漫です。
ここまできたので、同じモジュールを再購入して出直しを図ろうかと。
しかし、Primer2を触り始めてから、本体を壊して2個目を買ったりGPSモジュールを2個壊したりとどうも縁起が悪いので、mbedに寄り道してみようかしら、、と思っています。
最近のコメント