Raw data format of GNSS receivers (NovAtel, bynav, and unicore communications receivers)

category: gnss

Data exchange of navigation satellite receivers

NMEA-0183 format (text data) is the de facto standard and is widely used for data representation in GNSS (global navigation satellite system) satellite navigation.

For precision satellite positioning such as RTK (realtime kinematic) which can express satellite signals more accurately, RTCM (Radio Technical Commission for Maritime Services) format (binary data) and RINEX (receiver independent exchange) format (text data) format are used.

Raw data format in GNSS receivers

To help users get more details of their satellite signals, receiver manufacturers sometimes make it possible to output the raw satellite signal message data received and the intermediate data processed by the receiver. These raw data not only help us understand positioning, but also have potential for new applications.

Such raw data is proprietary to the receiver manufacturer. The raw data format is often different for each receiver manufacturer, but some are compatible with specific manufacturers. The bynav C1-FS receiver and unicore UM982 receiver have the raw data format and the commands compatible with NovAtel OEM729 receiver ones.

I use RTKLIB ver.2.4.3b34 for the output data processing of the GNSS receiver. I am very happy that RTKLIB’s NovAtel receiver (OEM7) settings are available for raw data processing of C1-FS receiver and UM982 receiver.

However, raw data output commands between different receivers are not always fully compatible. Professor Tomoji Takasu of Tokyo University of Marine Science and Technology, who is also the creator of RTKLIB, added additional raw data decoding function to the NovAtel receiver settings for using bynav receivers.

Compatibility of NovAtel raw data format

Therefore, the compatibility of raw data output commands between RTKLIB’s NovAtel receiver settings, NovAtel receivers, bynav receivers, and unicore receivers was tabulated using Microsoft Excel (novatel_binary.xlsx). Using Microsoft Excel’s filter button is convenient because you can, for example, extract only the commands that RTKLIB supports.

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

When handling the binary data of this NovAtel receiver, add b to the end of this command name in addition to log. For example, to use the rangecmp command to output observed pseudorange data in compressed format, send the string log rangecmpb ontime 0.5 to the receiver. Here, ontime 0.5 means output observation data every 0.5 seconds.

Some of these commands are assigned a message ID. Here, if this message ID is the same between different receivers, they are counted as the same message. Even if the message ID is the same, the format may be different between different receivers.

NovAtel receivers have a large number of raw data display commands. Therefore, bynav receivers, unicore receivers, and all the commands supported by RTKLIB are listed, and the correspondence with the NovAtel receiver’s message ID is examined. Therefore, the NovAtel receiver raw data display commands listed here are part of a whole.

From this table, we can see that there are messages with the same message ID but different command names, and messages with the same command name but different message IDs (original messages).

The table also states that the Galileo satellite ephemeris output command galephemeris (message ID 1122) can be interpreted by NovAtel receivers. The OEM7 Commands and Logs Reference Manual v22 (released on November 2022) no longer describes this command. However, since galephemeris can be used even with the latest firmware (7.08.14) at the moment, it is indicated that it can be interpreted in this table.(added on 2023-01-24)

In fact, at the moment, the control command and raw data format of the unicore receiver are not published on the manufacturer’s site, and the materials found on the Internet are used. Therefore, it is inaccurate for unicore receivers (for example, there are two message ids for OBSVM). I will try them with my unicore UM982 receiver.

NovAtel raw data display code

I wrote a Python code to display the message name of NovAtel raw data. Save the following code as a filename such as 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
            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
            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:
        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')


Raw data may contain secret messages from navigation satellite operators. I would like to analyze the raw data bit by bit.