GNSS受信機の生データフォーマット(NovAtel受信機・bynav受信機・unicore受信機)
測位衛星受信機のデータ交換
GNSS(global navigation satellite system)衛星測位でのデータ表現には、事実上の標準(デファクトスタンダード)とも言えるNMEA(エヌメア)-0183形式(テキストデータ)が広く用いられています。
RTK(realtime kinematic)などの精密衛星測位では、衛星信号をより正確に表現できるRTCM(Radio Technical Commission for Maritime Services)形式(バイナリデータ)や、RINEX(ライネックス、receiver independent exchange format)形式(テキストデータ)によるデータ表現が用いられます。
GNSS受信機の生データフォーマット
ユーザが衛星信号をより活用できるよう、受信機メーカは、受信した未加工の衛星信号メッセージデータや、受信機が処理した中間データを、外部に出力できるようにしていることがあります。これらの生データは、測位の理解に役立つだけでなく、新しい応用の可能性があります。
このような生データは、受信機メーカ独自のものです。その生データフォーマットは、受信機メーカごとに異なることが多いものの、一部には特定メーカとの互換性のあることがあります。私が購入したbynav C1-FS受信機や、unicore UM982受信機は、NovAtel(ノバテル)OEM729受信機との間で、生データ出力項目の一部に互換性があります。
私は、GNSS受信機の出力データ処理にRTKLIB(アールティーケィリブ) ver.2.4.3b34を利用しています。C1-FS受信機や、UM982受信機の生データ処理に、RTKLIBのNovAtel受信機(OEM7)の設定が利用できて、とても嬉しいです。
しかしながら、異なる受信機間の生データ出力コマンドは、完全に互換性があるとは限りません。RTKLIB作者でもある高須知二先生(東京海洋大学)は、bynav受信機利用のために、NovAtel受信機設定にさらなる生データ復号機能を追加されています。
NovAtel生データフォーマットの互換性
そこで、RTKLIBのNovAtel受信機設定、NovAtel受信機、bynav受信機、および、unicore受信機との間の、生データ出力コマンドの互換性をMicrosoft Excelにて集計しました(novatel_binary.xlsx)。Microsoft Excelのフィルタボタンを使えば、例えば、RTKLIBにて対応しているコマンドのみを抽出するなどの操作ができて、便利です。
このNovAtel受信機のバイナリデータを扱うときには、log
に加えて、このコマンド名の末尾にバイナリを表すb
を加えます。例えば、観測擬似距離データを圧縮形式にて出力するrangecmp
コマンドの利用には、log rangecmpb ontime 0.5
という文字列を受信機に送信します。ここで、ontime 0.5は、0.5秒ごとに観測データを出力することを表します。
これらのコマンドの中には、メッセージIDが割り当てられているものがあります。ここでは、異なる受信機間で、このメッセージIDが同一であれば同一メッセージとして集計しています。メッセージIDが同一でも、異なる受信機間でフォーマットが異なる可能性があります。
NovAtel受信機には、膨大な生データ表示コマンドがあります。そこで、bynav受信機、unicore受信機、そしてRTKLIBで対応しているコマンドをすべて記載し、その中でNovAtel受信機のメッセージIDとの対応を調べています。そのため、ここで挙げたNovAtel受信機の生データ表示コマンドは、全体の中の一部です。
この表から、同じメッセージIDでも異なるコマンド名称がついていることや、同じコマンド名称でも異なるメッセージID(独自メッセージ)を持つメッセージがあることがわかります。
また、この表の中で、Galileo衛星エフェメリス出力コマンドgalephemeris
(メッセージID 1122)がNovAtel受信機にて解釈できるとしています。OEM7 Commands and Logs Reference Manual v22 (November 2022)に、このコマンドの記載はなくなっていて、代わりにF/NAV用galfnavephemeris
(メッセージID 1310)とI/NAV用galinavephemeris
(メッセージID 1309)を使うことになっているようです。しかし、現時点での最新ファームウェア(7.08.14)でもgalephemeris
を利用できていますので、この表でも解釈できると表示しています。(2023-01-24追記)
実は、現時点にて、unicore受信機の制御コマンドや生データフォーマットがメーカサイトに公開されていなくて、インターネット上で見つけた資料をもとに作成しました。そのため、unicore受信機のものに関しては不正確です(例えば、OBSVM
のメッセージIDが2つあります)。これから、実際に私のunicore UM982受信機で試してみようと思います。
NovAtel生データ表示コード
NovAtel生データのメッセージ名を表示するPytonコードを書いてみました。次のコードを、例えばnovdump.py
などのファイル名にて保存します。
#! /usr/bin/env python3
# -*- coding: utf-8 -*-
#
# novdump.py: NovAtel receiver raw message dump
#
# Copyright (c) 2022 Satoshi Takahashi, all rights reserved.
#
# Released under BSD 2-clause license.
import sys
class Nov:
def read_nov(self):
sync = [b'0x00' for i in range(3)]
ok = False
try:
while not ok:
b = sys.stdin.buffer.read(1)
if not b:
return False
sync = sync[1:3] + [b]
if sync == [b'\xaa', b'\x44', b'\x12']:
ok = True
head = sys.stdin.buffer.read(1+2+1+1+2+2+1+1+2+4+4+2+2+4)
if not head:
return False
self.parse_head(head)
payload = sys.stdin.buffer.read(self.msg_len)
if not payload:
return False
self.payload = payload
crc = sys.stdin.buffer.read(4)
if not crc:
return False
except KeyboardInterrupt:
sys.exit()
return True
def parse_head(self, head):
pos = 0
self.head_len = int.from_bytes(head[pos:pos+1], 'little')
pos += 1
self.msg_id = int.from_bytes(head[pos:pos+2], 'little')
pos += 2
self.msg_type = int.from_bytes(head[pos:pos+1], 'little')
pos += 1
self.port = int.from_bytes(head[pos:pos+1], 'little')
pos += 1
self.msg_len = int.from_bytes(head[pos:pos+2], 'little')
pos += 2
self.seq = int.from_bytes(head[pos:pos+2], 'little')
pos += 2
self.t_idle = int.from_bytes(head[pos:pos+1], 'little')
pos += 1
self.t_stat = int.from_bytes(head[pos:pos+1], 'little')
pos += 1
self.gpsw = int.from_bytes(head[pos:pos+2], 'little')
pos += 2
self.gpst = int.from_bytes(head[pos:pos+4], 'little') / 1e4
pos += 4
self.stat = int.from_bytes(head[pos:pos+4], 'little')
pos += 4
self.reserved = int.from_bytes(head[pos:pos+2], 'little')
pos += 2
self.ver = int.from_bytes(head[pos:pos+2], 'little')
pos += 2
self.response = int.from_bytes(head[pos:pos+4], 'little')
pos += 4
def msgid(self):
return self.msg_id
def msgname(self, msg_id=0):
if msg_id == 0:
msg_id = self.msg_id
msg_name = 'unknown'
if msg_id == 43:
msg_name ='RANGE'
elif msg_id == 41:
msg_name = 'RAWEPHEM'
elif msg_id == 8:
msg_name = 'IONUTC'
elif msg_id == 723:
msg_name = 'GLOEPHEMERIS'
elif msg_id == 1330:
msg_name = 'QZSSRAWSUBFRAME'
elif msg_id == 1347:
msg_name ='QZSSIONUTC'
elif msg_id == 1122:
msg_name ='GALEPHEMERIS'
elif msg_id == 1696:
msg_name ='BDSEPHEMERIS'
elif msg_id == 2123:
msg_name ='NAVICEPHEMERIS'
elif msg_id == 140:
msg_name = 'RANGECMP'
elif msg_id == 287:
msg_name ='RAWWAASFRAME'
elif msg_id == 973:
msg_name = 'RAWSBASFRAME'
elif msg_id == 1127:
msg_name = 'GALIONO'
elif msg_id == 1121:
msg_name = 'GALCLOCK'
elif msg_id == 1331:
msg_name = 'QZSSRAWEPHEM'
return msg_name
def msglen(self):
return self.msg_len
if __name__ == '__main__':
nov = Nov()
while nov.read_nov():
print(f'MT{nov.msgid():<4d} {nov.msgname():17s} {nov.msglen()} bytes')
# EOF
生データには、測位衛星運用者からの秘密のメッセージが込められているかもしれません。少しずつ生データを解析してみようと思います。