GNSS受信機の生データフォーマット(NovAtel受信機・bynav受信機・unicore受信機)

categories: gnss

測位衛星受信機のデータ交換

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にて対応しているコマンドのみを抽出するなどの操作ができて、便利です。

compatibility of raw data format among NovAtel, bynav, and unicore receiver and RTKLIB’s NovAtel driver

この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

生データには、測位衛星運用者からの秘密のメッセージが込められているかもしれません。少しずつ生データを解析してみようと思います。