diff --git a/README.md b/README.md index a97af6a..ac955a8 100644 --- a/README.md +++ b/README.md @@ -284,7 +284,7 @@ Streaming terminated, 93 messages processed with 0 errors. ### Installing gnssstreamer as a systemd service on Linux -This example should work for most Linux distributions running `systemd` and `python3>=3.8`, including Raspberry Pi OS (*substitute `dnf` for `apt` as necessary*). +This example should work for most Linux distributions running `systemd` and `python3>=3.10`, including Raspberry Pi OS (*substitute `dnf` or `pacman` for `apt` as necessary*). The example `gnssstreamer.conf` file will set up `gnssstreamer` as a multi-client TCP socket server accessible on `hostip:50012` (*check TCP port 50012 is allowed through any active firewall*). @@ -499,11 +499,11 @@ class pygnssutils.rinex_conv.RinexConvertor(app, rinex_version, rinex_type, gnss A command line utility and Python class `RinexConverter` to convert binary GNSS data logs to RINEX text file format. -**NB: RINEX conversion is an experimental work in progress (*contributions and feedback welcome*). The current ALPHA release implements the following functionality:** +**NB: RINEX conversion is an experimental work in progress (*contributions and feedback welcome*). The current release implements the following functionality:** 1. RINEX versions 3.05 and 4.02. 1. Convert binary UBX RXM-RAW or RXM-RAWX (raw observation) data from u-blox receivers (e.g. ZED-F9P) to RINEX Observation file format. -1. Convert binary RXM-SFRBX (navigation subframe) data from u-blox receivers to RINEX Navigation file format. **Currently supports GPS LNAV/CNAV, GAL FNAV/INAV, BDS D1/D2 & GLO L1OF**, but the underlying `RinexConverterNavigation` class will be enhanced in future releases. +1. Convert binary RXM-SFRBX (navigation subframe) data from u-blox receivers to RINEX Navigation file format. 1. Convert RTCM3 Ephemerides messages (1019, 1020, 1041-1046) from any source (including NTRIP caster or RTK base station receiver) to RINEX Navigation file format. 1. Convert NMEA MWD (wind speed and direction) and XDR (temperature and pressure) sensor data to RINEX Meteorology file format. diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index def0a53..9956fcf 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,5 +1,10 @@ # pygnssutils +### RELEASE 1.2.5 + +1. Further enhancements and bug fixes to RINEX conversion routines. +1. Add support for SBAS L1CA, QZSS LNAV/CNAV, IRNSS (NAVIC) LNAV. + ### RELEASE 1.2.4 1. Further enhancements and bug fixes to RINEX conversion routines. diff --git a/docs/pygnssutils.rst b/docs/pygnssutils.rst index 1c92636..f963e0c 100644 --- a/docs/pygnssutils.rst +++ b/docs/pygnssutils.rst @@ -116,6 +116,70 @@ pygnssutils.rawnav module :undoc-members: :show-inheritance: +pygnssutils.rawnav\_reader module +--------------------------------- + +.. automodule:: pygnssutils.rawnav_reader + :members: + :undoc-members: + :show-inheritance: + +pygnssutils.rawnav\_subframes\_bds module +----------------------------------------- + +.. automodule:: pygnssutils.rawnav_subframes_bds + :members: + :undoc-members: + :show-inheritance: + +pygnssutils.rawnav\_subframes\_gal module +----------------------------------------- + +.. automodule:: pygnssutils.rawnav_subframes_gal + :members: + :undoc-members: + :show-inheritance: + +pygnssutils.rawnav\_subframes\_glo module +----------------------------------------- + +.. automodule:: pygnssutils.rawnav_subframes_glo + :members: + :undoc-members: + :show-inheritance: + +pygnssutils.rawnav\_subframes\_gps module +----------------------------------------- + +.. automodule:: pygnssutils.rawnav_subframes_gps + :members: + :undoc-members: + :show-inheritance: + +pygnssutils.rawnav\_subframes\_irn module +----------------------------------------- + +.. automodule:: pygnssutils.rawnav_subframes_irn + :members: + :undoc-members: + :show-inheritance: + +pygnssutils.rawnav\_subframes\_qzs module +----------------------------------------- + +.. automodule:: pygnssutils.rawnav_subframes_qzs + :members: + :undoc-members: + :show-inheritance: + +pygnssutils.rawnav\_subframes\_sba module +----------------------------------------- + +.. automodule:: pygnssutils.rawnav_subframes_sba + :members: + :undoc-members: + :show-inheritance: + pygnssutils.rinex\_conv module ------------------------------ @@ -172,38 +236,6 @@ pygnssutils.rinex\_helpers module :undoc-members: :show-inheritance: -pygnssutils.rinex\_subframes\_bds module ----------------------------------------- - -.. automodule:: pygnssutils.rinex_subframes_bds - :members: - :undoc-members: - :show-inheritance: - -pygnssutils.rinex\_subframes\_gal module ----------------------------------------- - -.. automodule:: pygnssutils.rinex_subframes_gal - :members: - :undoc-members: - :show-inheritance: - -pygnssutils.rinex\_subframes\_glo module ----------------------------------------- - -.. automodule:: pygnssutils.rinex_subframes_glo - :members: - :undoc-members: - :show-inheritance: - -pygnssutils.rinex\_subframes\_gps module ----------------------------------------- - -.. automodule:: pygnssutils.rinex_subframes_gps - :members: - :undoc-members: - :show-inheritance: - pygnssutils.socket\_server module --------------------------------- diff --git a/examples/process_rxmsfrbx_frames.py b/examples/process_rxmsfrbx_frames.py index 9c080d4..fa128e1 100644 --- a/examples/process_rxmsfrbx_frames.py +++ b/examples/process_rxmsfrbx_frames.py @@ -15,15 +15,20 @@ # pylint: disable = invalid-name from pygnssutils import GNSSReader -from pygnssutils.rawnav import RawNav, RawNavReader -from pygnssutils.rinex_globals import LNAV, CNAV, FNAV, L1OF, INAV, D1, D2, START, TARGET -from pygnssutils.rinex_subframes_gps import GPS_SUBFRAMEACQ_MAP -from pygnssutils.rinex_subframes_gal import GAL_SUBFRAMEACQ_MAP -from pygnssutils.rinex_subframes_bds import BDS_SUBFRAMEACQ_MAP -from pygnssutils.rinex_subframes_glo import GLO_SUBFRAMEACQ_MAP +from pygnssutils.rawnav import RawNav +from pygnssutils.rawnav_reader import RawNavReader +from pygnssutils.rinex_globals import L1CA, LNAV, CNAV, FNAV, L1OF, INAV, D1, D2, START, TARGET +from pygnssutils.rawnav_subframes_gps import GPS_SUBFRAMEACQ_MAP +from pygnssutils.rawnav_subframes_gal import GAL_SUBFRAMEACQ_MAP +from pygnssutils.rawnav_subframes_bds import BDS_SUBFRAMEACQ_MAP +from pygnssutils.rawnav_subframes_glo import GLO_SUBFRAMEACQ_MAP +from pygnssutils.rawnav_subframes_sba import SBA_SUBFRAMEACQ_MAP +from pygnssutils.rawnav_subframes_qzs import QZS_SUBFRAMEACQ_MAP +from pygnssutils.rawnav_subframes_irn import IRN_SUBFRAMEACQ_MAP # INFILE = "pygpsdata-rxmsfrbx.log" -INFILE = "/Users/steve/Downloads/pygpsdata-20260602101842.log" +# INFILE = "/Users/steve/Downloads/pygpsdata-20260602101842.log" +INFILE = "/Users/steve/Downloads/pygpsdata-20260611094239.ubx" gps = 0 navframes = {} @@ -35,7 +40,12 @@ # sfrmap = GAL_SUBFRAMEACQ_MAP[INAV] # subframe payload definitions # sfrmap = BDS_SUBFRAMEACQ_MAP[D1] # subframe payload definitions # sfrmap = BDS_SUBFRAMEACQ_MAP[D2] # subframe payload definitions -sfrmap = GLO_SUBFRAMEACQ_MAP[L1OF] # subframe payload definitions +# sfrmap = GLO_SUBFRAMEACQ_MAP[L1OF] # subframe payload definitions +sfrmap = SBA_SUBFRAMEACQ_MAP[L1CA] # subframe payload definitions +# sfrmap = QZS_SUBFRAMEACQ_MAP[LNAV] # subframe payload definitions +# sfrmap = QZS_SUBFRAMEACQ_MAP[CNAV] # subframe payload definitions +# sfrmap = QZS_SUBFRAMEACQ_MAP[CNV2] # subframe payload definitions +# sfrmap = IRN_SUBFRAMEACQ_MAP[L1CA] # subframe payload definitions subframes = {} with open(INFILE, "rb") as stream: @@ -46,13 +56,14 @@ continue if parsed.identity == "RXM-SFRBX": rxm += 1 - #if parsed.gnssId == 0 and parsed.sigId in (0,): # GPS LNAV: + # if parsed.gnssId == 0 and parsed.sigId in (0,): # GPS LNAV: # if parsed.gnssId == 0 and parsed.sigId in (3,4,6,7,): # GPS CNAV: # if parsed.gnssId == 2 and parsed.sigId in (3,): # GAL FNAV: # if parsed.gnssId == 2 and parsed.sigId in (1,5): # GAL INAV: # if parsed.gnssId == 3 and parsed.sigId in (0,2,4,): # BDS D1: # if parsed.gnssId == 3 and parsed.sigId in (1,3,10,): # BDS D2: - if parsed.gnssId == 6 and parsed.sigId in (0,): # GAL L1OF: + # if parsed.gnssId == 6 and parsed.sigId in (0,): # GLO L1OF: + if parsed.gnssId == 1 and parsed.sigId in (0,): # SBA L1CA: gps += 1 # extract the subframe from the RXM-SFRBX message sfrdata = rnr.process_rxm_sfrbx(parsed) diff --git a/src/pygnssutils/_version.py b/src/pygnssutils/_version.py index 00aaaa6..8b74d79 100644 --- a/src/pygnssutils/_version.py +++ b/src/pygnssutils/_version.py @@ -8,4 +8,4 @@ :license: BSD 3-Clause """ -__version__ = "1.2.4" +__version__ = "1.2.5" diff --git a/src/pygnssutils/rawnav.py b/src/pygnssutils/rawnav.py index 714cc2c..c4daee2 100644 --- a/src/pygnssutils/rawnav.py +++ b/src/pygnssutils/rawnav.py @@ -1,15 +1,9 @@ """ rawnav.py -Raw navigation data container and reader classes. - -The RawNavReader class implements methods to facilitate acquisition -of NAV subframe data from UBX RXM-SFRBX messages. - The RawNav class parses and stores the individual attributes (ephemerides, ionospheric & clock corrections, etc.) of one -or more raw GNSS NAV subframes, e.g. as a precursor to RINEX -conversion. +or more raw GNSS NAV subframes. Once a RawNav object is instantiated, the `parse` function can be invoked repeatedly to collate data from separate sequential @@ -19,39 +13,27 @@ An `subframeacq` bitfield signifies which subframe/page IDs have been acquired, and hence whether or not the RawNav frame contains -sufficient information to be converted to a NAV record. - -A boolean `sequence` argument determines whether subframes are -processed as a contiguous sequence e.g. for GNSS where MSB and LSB -attributes are held in separate, sequential subframes. +sufficient information to be converted to a NAV record e.g. as a +precursor to RINEX conversion. The objective is to handle any GNSS subframe format for which: - - data is available as a raw, unpadded little-endian integer. - - data definition dictionary has been transcribed from the relevant + - the complete, unpadded subframe is represented as an unsigned little-endian + integer. + - the subframe definition dictionary has been transcribed from the relevant GNSS ICD (Interface Control Document) with standardized ascii - field names e.g. `omegadot`, `sqrta`, `cus`, etc. + field names e.g. `omegadot`, `sqrta`, `cus`, `tauc`, etc. -Format of data definition dictionary:: +Format of subframe definition dictionary:: dict[field_name, tuple[offset, length, encoding, scaling] where offset and length are in bits (see, for example, -`rinex_subframes_gps.py`). - -MSB and LSB field names MUST be suffixed "_msb" and "_lsb" -respectively - the `parse` function will automatically combine them. +`rawnav_subframes_gps.py`). -RXM-SFRBX structures for each GNSS are documented in section 3.15.1 Broadcast -navigation data: - -https://www.u-blox.com/sites/default/files/ZED-F9P_IntegrationManual_UBX-18010802.pdf - -NB: Alpha release currently implements: - - GPS LNAV, CNAV - - GAL FNAV, INAV - - BDS D1, D2 - - GLO L1OF +MSB, ISB (intermediate bits) and LSB field names MUST be suffixed "_msb", +"_isb" and "_lsb" respectively - the `parse` function will automatically +combine them. Created on 20 Apr 2026 @@ -60,27 +42,15 @@ :license: BSD 3-Clause """ -# pylint: disable=unused-argument, unused-variable, too-many-arguments, too-many-positional-arguments +# pylint: disable=too-many-arguments, too-many-positional-arguments import struct from logging import getLogger -from types import NoneType from typing import Literal -from pyubx2 import UBXMessage - from pygnssutils.exceptions import RINEXProcessingError -from pygnssutils.rinex_globals import ( - BDS, - GAL, - GLO, - GPS, - IRN, - QZS, - SBA, - UBXRINEXGNSS, -) -from pygnssutils.rinex_helpers import get_obscode_ubx +from pygnssutils.rinex_globals import GLO +from pygnssutils.rinex_helpers import get_svcode IS1 = "_is1" """IS1 field name suffix (between MSB and ISB)""" @@ -101,7 +71,7 @@ WN = "wn" """WN (week number) field name - used to establish epoch""" -PREAMBLE = "preamble" +PREAMBLE = "_preamble" VALPREAMBLE = "_valid_preamble" D = "D" # IEEE 754 64-bit double float F = "F" # IEEE 754 32-bit float @@ -136,7 +106,7 @@ def __init__( self._gnss = gnss self._svid = svid self._sigcode = sigcode - if gnss != "R": + if gnss != GLO: self.wn = -1 self.toc = -1 self.tow = -1 @@ -164,25 +134,24 @@ def parse( :param dict[str, tuple[int, int, str, int]] subframedef: subframe \ definition dictionary (from GNSS ICD) :param int subframeacq: subframe acquisition bitmask - :param bool sequence: process subframe as part of a contiguous sequence (False) + :param bool sequence: process subframe as part of a contiguous sequence (True) :raises: RINEXProcessingError """ - # pylint: disable=too-many-locals try: # get exemplary preamble value if one is available valpre = subframedef.pop(VALPREAMBLE, 0) - # validate subframe length in bits + # get total subframe length in bits offset, bitlen, _, _ = list(subframedef.values())[-1] sfrlen = offset + bitlen - # parse each attribute in subframe, combining MSB and LSB fields - # where appropriate + # parse each attribute in subframe, combining MSB, ISB and + # LSB fields where appropriate for att, (offset, length, encoding, scaling) in subframedef.items(): - if att[0:1] == "_": # ignore non-data bits + if att[0:1] == "_": # ignore non-data attributes continue bits = data >> (sfrlen - offset - length) & (2**length - 1) @@ -203,23 +172,18 @@ def parse( self._isb[att] = (bits, length, encoding, scaling) continue if att[-4:].lower() == LSB: # least significant bits - msbbits, msblen, _, _ = self._msb.pop( - f"{att[:-4]}{MSB}", (0, 0, 0, 0) - ) - is1bits, is1len, _, _ = self._is1.pop( - f"{att[:-4]}{IS1}", (0, 0, 0, 0) - ) - isbbits, isblen, _, _ = self._isb.pop( - f"{att[:-4]}{ISB}", (0, 0, 0, 0) - ) + attns = att[:-4] + msbbits, msblen, _, _ = self._msb.pop(f"{attns}{MSB}", (0, 0, 0, 0)) + is1bits, is1len, _, _ = self._is1.pop(f"{attns}{IS1}", (0, 0, 0, 0)) + isbbits, isblen, _, _ = self._isb.pop(f"{attns}{ISB}", (0, 0, 0, 0)) bits = ( (msbbits << (is1len + isblen + length)) - + (is1bits << isblen + length) + + (is1bits << (isblen + length)) + (isbbits << length) + bits ) - if msblen: - att = att[:-4] + if msblen: # if combining with msb... + att = attns # strip "_lsb" suffix length += msblen + is1len + isblen val = self._bits2val(bits, length, encoding, scaling) @@ -256,7 +220,7 @@ def _bits2val( :param int vali: value as raw integer :param int length: length in bits - :param str encoding: bit encoding e.g. U, S, F + :param str encoding: bit encoding e.g. U, S, N, F :param int scaling: scaling factor (0 = no scaling) :return: decoded value :rtype: int | float @@ -311,12 +275,13 @@ def identity(self) -> str: :rtype: str """ - return f"{self._gnss}{self._svid:02d}{self._sigcode}" + svcode = get_svcode(self._gnss, self._svid, leadzero=True) + return f"{svcode}{self._sigcode}" @property def gnss(self) -> str: """ - Getter for GNSS code. + Getter for GNSS code e.g. "G" :return: gnss :rtype: str @@ -327,7 +292,7 @@ def gnss(self) -> str: @property def svid(self) -> int: """ - Getter for SV id. + Getter for SV id e.g. 14 :return: svid :rtype: int @@ -344,7 +309,7 @@ def svcode(self) -> str: :rtype: str """ - return f"{self._gnss}{self._svid:>2}" # no leading zero + return get_svcode(self._gnss, self._svid, leadzero=False) @property def sigcode(self) -> str: @@ -373,402 +338,3 @@ def subframeacq(self) -> int: """ return self._subframeacq - - -class RawNavReader: - """ - Raw Navigation Reader Class. - """ - - # pylint: disable=too-many-instance-attributes - - def __init__(self, **kwargs): # pylint: disable=unused-argument - """ - Constructor. - - :param dict kwargs: optional keyword arguments - """ - - def process_rxm_sfrbx( - self, data: UBXMessage - ) -> dict[str, str | int | float | NoneType]: - """ - Reassemble subframe from individual UBX RXM-SFRBX dwrds. - - :param UBXMessage data: parsed UBX RXM-SFRBX message - :return: dict of subframe attributes - :rtype: dict[str, str | int | float | NoneType] - :raises: RINEXProcessingError - """ - - if isinstance(data, UBXMessage): - if data.identity != "RXM-SFRBX": - raise RINEXProcessingError( - f"Data must be UBX RXM-SFRBX message - got {type(data)}" - ) - - sfrdata = {} - try: - gnss = UBXRINEXGNSS[data.gnssId] - svid = data.svId - numw = data.numWords - sigcode = get_obscode_ubx(data.gnssId, data.sigId) - except KeyError as err: - raise RINEXProcessingError( - f"Unrecognised GNSS or Signal code: {data.gnssId=}, {data.sigId=}" - ) from err - - if gnss == GPS: - sfrdata = self._process_rxm_sfrbx_gps(gnss, svid, sigcode, numw, data) - elif gnss == GAL: - sfrdata = self._process_rxm_sfrbx_gal(gnss, svid, sigcode, numw, data) - elif gnss == BDS: - sfrdata = self._process_rxm_sfrbx_bds(gnss, svid, sigcode, numw, data) - elif gnss == GLO: - sfrdata = self._process_rxm_sfrbx_glo(gnss, svid, sigcode, numw, data) - elif gnss == SBA: - sfrdata = self._process_rxm_sfrbx_sba(gnss, svid, sigcode, numw, data) - elif gnss == QZS: - sfrdata = self._process_rxm_sfrbx_qzs(gnss, svid, sigcode, numw, data) - elif gnss == IRN: - sfrdata = self._process_rxm_sfrbx_irn(gnss, svid, sigcode, numw, data) - return sfrdata - - def _process_rxm_sfrbx_gps( - self, gnss: str, svid: int, sigcode: str, numw: int, data: UBXMessage - ) -> dict[str, str | int | float | NoneType]: - """ - Reassemble GPS subframe from individual UBX RXM-SFRBX dwrds. - - :param str gnss: RINEX gnss code - :param int svid: SV - :param str sigcode: RINEX sig code e.g. '1C' - :param UBXMessage data: parsed UBX RXM-SFRBX message - :return: dict of subframe attributes - :rtype: dict[str, str | int | float | NoneType] - :raises: RINEXProcessingError - """ - - subframe = 0 - subframeid = 0 - subframepageid = 0 - - # for GPS LNAV, subframe = 10 * 30 bits, with each 32-bit dwrd padded with 2 bits at end - if sigcode == "1C": # GPS LNAV - for i in range(numw): - wrd = getattr(data, f"dwrd_{i+1:02d}") & 0xFFFFFFFC >> 2 - subframe += wrd << (30 * (numw - 1 - i)) - subframeid = (subframe >> 248) & 0b111 - if subframeid in (4, 5): - subframepageid = subframe >> 232 & 0b111111 - - # for GPS CNAV, subframe = 3 * 100 bits, final 20 bits of 320 bit dwrd is padding - elif sigcode in ("2L", "2S", "5I", "5Q"): # GPS CNAV - for i in range(numw): - wrd = getattr(data, f"dwrd_{i+1:02d}") - subframe += wrd << (32 * (numw - 1 - i)) - subframe = ( - (subframe >> 20) - & 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF - ) # (2**300 - 1) - subframeid = (subframe >> 280) & 0b111111 - - return { - "gnss": gnss, - "svid": svid, - "sigcode": sigcode, - "subframeid": subframeid, - "subframepageid": subframepageid, - "subframe": subframe, - } - - def _process_rxm_sfrbx_gal( - self, gnss: str, svid: int, sigcode: str, numw: int, data: UBXMessage - ) -> dict[str, str | int | float | NoneType]: - """ - Reassemble GALILEO subframe from individual UBX RXM-SFRBX dwrds. - - :param str gnss: RINEX gnss code - :param int svid: SV - :param str sigcode: RINEX sigid e.g. '5I' - :param UBXMessage data: parsed UBX RXM-SFRBX message - :return: dict of subframe attributes - :rtype: dict[str, str | int | float | NoneType] - :raises: RINEXProcessingError - """ - - subframe = 0 - subframeid = 0 - subframepageid = 0 - - # for GAL FNAV, subframe = 244 bits, - # 8 * 32 bit dwrds with 12 bits padding at end - if sigcode == "5I": # GAL FNAV - for i in range(numw): - wrd = getattr(data, f"dwrd_{i+1:02d}") - subframe += wrd << (32 * (numw - 1 - i)) - subframe = ( - subframe >> 12 - ) & 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF # (2**244 - 1) - subframeid = (subframe >> 238) & 0b111111 - - # for GAL INAV, subframe = 256 bits, 8 * 32 bit dwrds, - # with word data separated into 112 msb and 16 lsb - # (see GAL_INAV_SUBFRAME) - elif sigcode in ("1B", "7I"): # GAL INAV - supsubframe = 0 - for i in range(numw): - wrd = getattr(data, f"dwrd_{i+1:02d}") - supsubframe += wrd << (32 * (numw - 1 - i)) - subframe_msb = ( - supsubframe >> 142 - ) & 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFF # (2**112 - 1) - subframe_lsb = (supsubframe >> 110) & 0xFFFF # (2**16 - 1) - subframe = (subframe_msb << 16) | subframe_lsb - subframeid = (subframe >> 122) & 0b111111 - - return { - "gnss": gnss, - "svid": svid, - "sigcode": sigcode, - "subframeid": subframeid, - "subframepageid": subframepageid, - "subframe": subframe, - } - - def _process_rxm_sfrbx_bds( - self, gnss: str, svid: int, sigcode: str, numw: int, data: UBXMessage - ) -> dict[str, str | int | float | NoneType]: - """ - Reassemble BEIDOU subframe from individual UBX RXM-SFRBX dwrds. - - :param str gnss: RINEX gnss code - :param int svid: SV - :param str sigcode: RINEX sig code e.g. '2I' - :param UBXMessage data: parsed UBX RXM-SFRBX message - :return: dict of subframe attributes - :rtype: dict[str, str | int | float | NoneType] - :raises: RINEXProcessingError - """ - - subframe = 0 - subframeid = 0 - subframepageid = 0 - d1d2 = 0 - - if sigcode in ("2I", "6I", "7I"): # BDS D1/D2 - if data.sigId in (1, 3, 10): # D2 - d1d2 = 2 - else: # D1 - d1d2 = 1 - for i in range(numw): - wrd = getattr(data, f"dwrd_{i+1:02d}") & 0xFFFFFFFC >> 2 - subframe += wrd << (30 * (numw - 1 - i)) - subframeid = (subframe >> 282) & 0b111 - if d1d2 == 1 and subframeid in (4, 5): - subframepageid = subframe >> 250 & 0b1111111 - elif d1d2 == 2 and subframeid in (1,): - subframepageid = subframe >> 254 & 0b1111 - - elif sigcode == "1D": # BDS CNV1 - pass # TODO - - elif sigcode == "5D": # BDS CNV2 - pass # TODO - - return { - "gnss": gnss, - "svid": svid, - "sigcode": sigcode, - "d1d2": d1d2, - "subframeid": subframeid, - "subframepageid": subframepageid, - "subframe": subframe, - } - - def _process_rxm_sfrbx_glo( - self, gnss: str, svid: int, sigcode: str, numw: int, data: UBXMessage - ) -> dict[str, str | int | float | NoneType]: - """ - Reassemble GLONASS subframe from individual UBX RXM-SFRBX dwrds. - - :param str gnss: RINEX gnss code - :param int svid: SV - :param str sigcode: RINEX sigid e.g. '1C' - :param UBXMessage data: parsed UBX RXM-SFRBX message - :return: dict of subframe attributes - :rtype: dict[str, str | int | float | NoneType] - :raises: RINEXProcessingError - """ - - subframe = 0 - subframeid = 0 - subframepageid = 0 - freqid = data.freqId - - # for GLO, subframe = 85 bits, - # 3 * 32 bit dwrds with 11 bits padding at end, - # plus a receiver-generated 4th dwrd containing superframe and frame ids - if sigcode in ("1C",): # GLO L1OF - for i in range(numw): - wrd = getattr(data, f"dwrd_{i+1:02d}") - subframe += wrd << (32 * (numw - 1 - i)) - # strip 4th dwrd, leaving 85 bit subframe - subframe = (subframe >> 43) & 0x1FFFFFFFFFFFFFFFFFFFFF # 2**85-1 - subframeid = (subframe >> 80) & 0b01111 - - return { - "gnss": gnss, - "svid": svid, - "sigcode": sigcode, - "subframeid": subframeid, - "subframepageid": subframepageid, - "subframe": subframe, - "freqid": freqid, - } - - def _process_rxm_sfrbx_sba( - self, gnss: str, svid: int, sigcode: str, numw: int, data: UBXMessage - ) -> dict[str, str | int | float | NoneType]: - """ - Reassemble SBAS subframe from individual UBX RXM-SFRBX dwrds. - - :param str gnss: RINEX gnss code - :param int svid: SV - :param str sigcode: RINEX sigcode e.g. '1C' - :param UBXMessage data: parsed UBX RXM-SFRBX message - :return: dict of subframe attributes - :rtype: dict[str, str | int | float | NoneType] - :raises: RINEXProcessingError - """ - - subframe = 0 - subframeid = 0 - subframepageid = 0 - svid -= 100 # adjust SV ID range - - # for SBAS, subframe = 250 bits, - # 8 * 32 bit dwrds with 6 bits padding at end - if sigcode == "1C": # SBAS L1C/A - for i in range(numw): - wrd = getattr(data, f"dwrd_{i+1:02d}") - subframe += wrd << (32 * (numw - 1 - i)) - subframe = ( - subframe >> 12 - ) & 0x3FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF # (2**250 - 1) - subframeid = (subframe >> 236) & 0b111111 - - return { - "gnss": gnss, - "svid": svid, - "sigcode": sigcode, - "subframeid": subframeid, - "subframepageid": subframepageid, - "subframe": subframe, - } - - def _process_rxm_sfrbx_qzs( - self, gnss: str, svid: int, sigcode: str, numw: int, data: UBXMessage - ) -> dict[str, str | int | float | NoneType]: - """ - Reassemble QZSS subframe from individual UBX RXM-SFRBX dwrds. - - :param str gnss: RINEX gnss code - :param int svid: SV - :param str sigcode: RINEX sigcode e.g. '1C' - :param UBXMessage data: parsed UBX RXM-SFRBX message - :return: dict of subframe attributes - :rtype: dict[str, str | int | float | NoneType] - :raises: RINEXProcessingError - """ - - subframe = 0 - subframeid = 0 - subframepageid = 0 - svid -= 192 # adjust SV ID range - - # for QZSS L1C/A, subframe = 10 * 30 bits, with each 32-bit dwrd padded with 2 bits at end - # same as GPS LNAV - if sigcode == "1C": # QZSS L1C/A - for i in range(numw): - wrd = getattr(data, f"dwrd_{i+1:02d}") & 0xFFFFFFFC >> 2 - subframe += wrd << (30 * (numw - 1 - i)) - subframeid = (subframe >> 248) & 0b111 - if subframeid in (4, 5): - subframepageid = subframe >> 232 & 0b111111 - - # for QZSS L1S, subframe = 250 bits, 8 * 32 bit dwrds with 6 bits padding at end - # same as SBAS L1C/A - if sigcode == "1Z": # QZSS L1S - for i in range(numw): - wrd = getattr(data, f"dwrd_{i+1:02d}") - subframe += wrd << (32 * (numw - 1 - i)) - subframe = ( - subframe >> 12 - ) & 0x3FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF # (2**250 - 1) - subframeid = (subframe >> 236) & 0b111111 - - # for QZSS L2C, L5I, subframe = 3 * 100 bits, final 20 bits of 320 bit dwrd is padding - # same as GPS CNAV - elif sigcode in ( - "2L", - "2S", - "5I", - ): # QZSS L2C, L5I - for i in range(numw): - wrd = getattr(data, f"dwrd_{i+1:02d}") - subframe += wrd << (32 * (numw - 1 - i)) - subframe = ( - (subframe >> 20) - & 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF - ) # (2**300 - 1) - subframeid = (subframe >> 280) & 0b111111 - - return { - "gnss": gnss, - "svid": svid, - "sigcode": sigcode, - "subframeid": subframeid, - "subframepageid": subframepageid, - "subframe": subframe, - } - - def _process_rxm_sfrbx_irn( - self, gnss: str, svid: int, sigcode: str, numw: int, data: UBXMessage - ) -> dict[str, str | int | float | NoneType]: - """ - Reassemble IRNSS (NAVIC) subframe from individual UBX RXM-SFRBX dwrds. - - :param str gnss: RINEX gnss code - :param int svid: SV - :param str sigcode: RINEX sigcode e.g. '5A' - :param UBXMessage data: parsed UBX RXM-SFRBX message - :return: dict of subframe attributes - :rtype: dict[str, str | int | float | NoneType] - :raises: RINEXProcessingError - """ - - subframe = 0 - subframeid = 0 - subframepageid = 0 - - # for IRN L5A, subframe = 3 * 100 bits, final 20 bits of 320 bit dwrd is padding - # same as GPS CNAV - if sigcode in ("5A",): # IRN L5A - for i in range(numw): - wrd = getattr(data, f"dwrd_{i+1:02d}") - subframe += wrd << (32 * (numw - 1 - i)) - subframe = ( - (subframe >> 20) - & 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF - ) # (2**300 - 1) - subframeid = (subframe >> 280) & 0b111111 - - return { - "gnss": gnss, - "svid": svid, - "sigcode": sigcode, - "subframeid": subframeid, - "subframepageid": subframepageid, - "subframe": subframe, - } diff --git a/src/pygnssutils/rawnav_reader.py b/src/pygnssutils/rawnav_reader.py new file mode 100644 index 0000000..e206e0b --- /dev/null +++ b/src/pygnssutils/rawnav_reader.py @@ -0,0 +1,449 @@ +""" +rawnav_reader.py + +The RawNavReader class implements methods to facilitate acquisition +of raw NAV subframe data from UBX RXM-SFRBX messages. + +RXM-SFRBX structures for each GNSS are documented in the "Broadcast +navigation data" section of the receiver's Integration Manual e.g. + +ZED-F9P: +https://www.u-blox.com/sites/default/files/ZED-F9P_IntegrationManual_UBX-18010802.pdf +ZED-X20P: +https://www.u-blox.com/sites/default/files/documents/ZED-X20P_IntegrationManual_UBXDOC-963802114-12901.pdf + +Created on 20 Apr 2026 + +:author: semuadmin (Steve Smith) +:copyright: semuadmin © 2026 +:license: BSD 3-Clause +""" + +# pylint: disable=too-many-arguments, too-many-positional-arguments, too-many-instance-attributes + +from types import NoneType + +from pyubx2 import UBXMessage + +from pygnssutils.exceptions import RINEXProcessingError +from pygnssutils.rinex_globals import ( + BDS, + GAL, + GLO, + GPS, + IRN, + QZS, + SBA, + UBXRINEXGNSS, +) +from pygnssutils.rinex_helpers import get_obscode_ubx + + +class RawNavReader: + """ + Raw Navigation Reader Class. + """ + + def __init__(self, **kwargs): # pylint: disable=unused-argument + """ + Constructor. + + :param dict kwargs: optional keyword arguments + """ + + def process_rxm_sfrbx( + self, data: UBXMessage + ) -> dict[str, str | int | float | NoneType]: + """ + Reassemble subframe from individual UBX RXM-SFRBX dwrds. + + :param UBXMessage data: parsed UBX RXM-SFRBX message + :return: dict of subframe attributes + :rtype: dict[str, str | int | float | NoneType] + :raises: RINEXProcessingError + """ + + if isinstance(data, UBXMessage): + if data.identity != "RXM-SFRBX": + raise RINEXProcessingError( + f"Data must be UBX RXM-SFRBX message - got {type(data)}" + ) + + sfrdata = {} + try: + gnss = UBXRINEXGNSS[data.gnssId] + svid = data.svId + numw = data.numWords + sigcode = get_obscode_ubx(data.gnssId, data.sigId) + except KeyError as err: + raise RINEXProcessingError( + f"Unrecognised GNSS or Signal code: {data.gnssId=}, {data.sigId=}" + ) from err + + if gnss == GPS: + sfrdata = self._process_rxm_sfrbx_gps(gnss, svid, sigcode, numw, data) + elif gnss == GAL: + sfrdata = self._process_rxm_sfrbx_gal(gnss, svid, sigcode, numw, data) + elif gnss == BDS: + sfrdata = self._process_rxm_sfrbx_bds(gnss, svid, sigcode, numw, data) + elif gnss == GLO: + sfrdata = self._process_rxm_sfrbx_glo(gnss, svid, sigcode, numw, data) + elif gnss == SBA: + sfrdata = self._process_rxm_sfrbx_sba(gnss, svid, sigcode, numw, data) + elif gnss == QZS: + sfrdata = self._process_rxm_sfrbx_qzs(gnss, svid, sigcode, numw, data) + elif gnss == IRN: + sfrdata = self._process_rxm_sfrbx_irn(gnss, svid, sigcode, numw, data) + return sfrdata + + def _process_rxm_sfrbx_gps( + self, gnss: str, svid: int, sigcode: str, numw: int, data: UBXMessage + ) -> dict[str, str | int | float | NoneType]: + """ + Reassemble GPS subframe from individual UBX RXM-SFRBX dwrds. + + - GPS LNAV subframe = 300 bits; 10 * 32-bit dwrds with each dwrd padded + with 2 bits at end + + - GPS CNAV subframe = 300 bits; 10 * 32-bit dwrds with 20 bits padding at end + + :param str gnss: RINEX gnss code e.g. "G" + :param int svid: SV + :param str sigcode: RINEX sig code e.g. '1C' + :param UBXMessage data: parsed UBX RXM-SFRBX message + :return: dict of subframe attributes + :rtype: dict[str, str | int | float | NoneType] + :raises: RINEXProcessingError + """ + + subframe = 0 + subframeid = 0 + subframepageid = 0 + dataid = 0 + + if sigcode == "1C": # GPS LNAV + for i in range(numw): + wrd = getattr(data, f"dwrd_{i+1:02d}") & 0xFFFFFFFC >> 2 + subframe += wrd << (30 * (numw - 1 - i)) + subframeid = (subframe >> 248) & 0b111 + if subframeid in (4, 5): + dataid = (subframe >> 238) & 0b11 + subframepageid = subframe >> 232 & 0b111111 + + elif sigcode in ("2L", "2S", "5I", "5Q"): # GPS CNAV + for i in range(numw): + wrd = getattr(data, f"dwrd_{i+1:02d}") + subframe += wrd << (32 * (numw - 1 - i)) + subframe = ( + (subframe >> 20) + & 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + ) # (2**300 - 1) + subframeid = (subframe >> 280) & 0b111111 + + return { + "gnss": gnss, + "svid": svid, + "sigcode": sigcode, + "subframeid": subframeid, + "dataid": dataid, + "subframepageid": subframepageid, + "subframe": subframe, + } + + def _process_rxm_sfrbx_gal( + self, gnss: str, svid: int, sigcode: str, numw: int, data: UBXMessage + ) -> dict[str, str | int | float | NoneType]: + """ + Reassemble GALILEO subframe from individual UBX RXM-SFRBX dwrds. + + - GAL FNAV subframe = 244 bits; 8 * 32-bit dwrds with 12 bits padding at end + + - GAL INAV subframe = 256 bits; 8 * 32-bit dwrds with subframe data separated + into 112 msb and 16 lsb (see GAL_INAV_SUBFRAME) + + :param str gnss: RINEX gnss code e.g. "E" + :param int svid: SV + :param str sigcode: RINEX sigid e.g. '5I' + :param UBXMessage data: parsed UBX RXM-SFRBX message + :return: dict of subframe attributes + :rtype: dict[str, str | int | float | NoneType] + :raises: RINEXProcessingError + """ + + subframe = 0 + subframeid = 0 + subframepageid = 0 + + if sigcode == "5I": # GAL FNAV + for i in range(numw): + wrd = getattr(data, f"dwrd_{i+1:02d}") + subframe += wrd << (32 * (numw - 1 - i)) + subframe = ( + subframe >> 12 + ) & 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF # (2**244 - 1) + subframeid = (subframe >> 238) & 0b111111 + + elif sigcode in ("1B", "7I"): # GAL INAV + supsubframe = 0 + for i in range(numw): + wrd = getattr(data, f"dwrd_{i+1:02d}") + supsubframe += wrd << (32 * (numw - 1 - i)) + subframe_msb = ( + supsubframe >> 142 + ) & 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFF # (2**112 - 1) + subframe_lsb = (supsubframe >> 110) & 0xFFFF # (2**16 - 1) + subframe = (subframe_msb << 16) | subframe_lsb + subframeid = (subframe >> 122) & 0b111111 + + return { + "gnss": gnss, + "svid": svid, + "sigcode": sigcode, + "subframeid": subframeid, + "subframepageid": subframepageid, + "subframe": subframe, + } + + def _process_rxm_sfrbx_bds( + self, gnss: str, svid: int, sigcode: str, numw: int, data: UBXMessage + ) -> dict[str, str | int | float | NoneType]: + """ + Reassemble BEIDOU subframe from individual UBX RXM-SFRBX dwrds. + + :param str gnss: RINEX gnss code e.g. "C" + :param int svid: SV + :param str sigcode: RINEX sig code e.g. '2I' + :param UBXMessage data: parsed UBX RXM-SFRBX message + :return: dict of subframe attributes + :rtype: dict[str, str | int | float | NoneType] + :raises: RINEXProcessingError + """ + + subframe = 0 + subframeid = 0 + subframepageid = 0 + d1d2 = 0 + + if sigcode in ("2I", "6I", "7I"): # BDS D1/D2 + if data.sigId in (1, 3, 10): # D2 + d1d2 = 2 + else: # D1 + d1d2 = 1 + for i in range(numw): + wrd = getattr(data, f"dwrd_{i+1:02d}") & 0xFFFFFFFC >> 2 + subframe += wrd << (30 * (numw - 1 - i)) + subframeid = (subframe >> 282) & 0b111 + if d1d2 == 1 and subframeid in (4, 5): + subframepageid = subframe >> 250 & 0b1111111 + elif d1d2 == 2 and subframeid in (1,): + subframepageid = subframe >> 254 & 0b1111 + + elif sigcode == "1D": # BDS CNV1 + pass # TODO + + elif sigcode == "5D": # BDS CNV2 + pass # TODO + + return { + "gnss": gnss, + "svid": svid, + "sigcode": sigcode, + "d1d2": d1d2, + "subframeid": subframeid, + "subframepageid": subframepageid, + "subframe": subframe, + } + + def _process_rxm_sfrbx_glo( + self, gnss: str, svid: int, sigcode: str, numw: int, data: UBXMessage + ) -> dict[str, str | int | float | NoneType]: + """ + Reassemble GLONASS subframe from individual UBX RXM-SFRBX dwrds. + + - GLO subframe = 85 bits; 3 * 32-bit dwrds with 11 bits padding at end, + plus a receiver-generated 4th dwrd containing superframe and frame ids + + :param str gnss: RINEX gnss code e.g. "R" + :param int svid: SV + :param str sigcode: RINEX sigid e.g. '1C' + :param UBXMessage data: parsed UBX RXM-SFRBX message + :return: dict of subframe attributes + :rtype: dict[str, str | int | float | NoneType] + :raises: RINEXProcessingError + """ + + subframe = 0 + subframeid = 0 + subframepageid = 0 + freqid = data.freqId + + if sigcode in ("1C",): # GLO L1OF + for i in range(numw): + wrd = getattr(data, f"dwrd_{i+1:02d}") + subframe += wrd << (32 * (numw - 1 - i)) + # strip padding & 4th dwrd, leaving 85 bit subframe + subframe = (subframe >> 43) & 0x1FFFFFFFFFFFFFFFFFFFFF # 2**85-1 + subframeid = (subframe >> 80) & 0b01111 + + return { + "gnss": gnss, + "svid": svid, + "sigcode": sigcode, + "subframeid": subframeid, + "subframepageid": subframepageid, + "subframe": subframe, + "freqid": freqid, + } + + def _process_rxm_sfrbx_sba( + self, gnss: str, svid: int, sigcode: str, numw: int, data: UBXMessage + ) -> dict[str, str | int | float | NoneType]: + """ + Reassemble SBAS subframe from individual UBX RXM-SFRBX dwrds. + + - SBAS subframe = 250 bits; 8 * 32-bit dwrds with 6 bits padding at end + + :param str gnss: RINEX gnss code e.g. "S" + :param int svid: SV + :param str sigcode: RINEX sigcode e.g. '1C' + :param UBXMessage data: parsed UBX RXM-SFRBX message + :return: dict of subframe attributes + :rtype: dict[str, str | int | float | NoneType] + :raises: RINEXProcessingError + """ + + subframe = 0 + subframeid = 0 + subframepageid = 0 + + if sigcode == "1C": # SBAS L1C/A + for i in range(numw): + wrd = getattr(data, f"dwrd_{i+1:02d}") + subframe += wrd << (32 * (numw - 1 - i)) + subframe = ( + subframe >> 6 + ) & 0x3FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF # (2**250 - 1) + subframeid = (subframe >> 236) & 0b111111 + + return { + "gnss": gnss, + "svid": svid, + "sigcode": sigcode, + "subframeid": subframeid, + "subframepageid": subframepageid, + "subframe": subframe, + } + + def _process_rxm_sfrbx_qzs( + self, gnss: str, svid: int, sigcode: str, numw: int, data: UBXMessage + ) -> dict[str, str | int | float | NoneType]: + """ + Reassemble QZSS subframe from individual UBX RXM-SFRBX dwrds. + + - QZSS LNAV L1C/A subframe = 300 bits; 10 * 32-bit dwrds with each + dwrd padded with 2 bits at end (same as GPS LNAV) + + - QZSS CNAV L2C, L5I, subframe = 300 bits; 10 * 32-bit dwrds with 20 bits padding + at end (same as GPS CNAV) + + - QZSS CNV2 L1S subframe = 250 bits; 8 * 32-bit dwrds with 6 bits padding + at end (same as SBAS L1C/A) + + :param str gnss: RINEX gnss code e.g. "J" + :param int svid: SV + :param str sigcode: RINEX sigcode e.g. '1C' + :param UBXMessage data: parsed UBX RXM-SFRBX message + :return: dict of subframe attributes + :rtype: dict[str, str | int | float | NoneType] + :raises: RINEXProcessingError + """ + + subframe = 0 + subframeid = 0 + subframepageid = 0 + dataid = 0 + + if sigcode == "1C": # QZSS L1C/A - LNAV + for i in range(numw): + wrd = getattr(data, f"dwrd_{i+1:02d}") & 0xFFFFFFFC >> 2 + subframe += wrd << (30 * (numw - 1 - i)) + subframeid = (subframe >> 248) & 0b111 + if subframeid in (4, 5): + dataid = (subframe >> 238) & 0b11 + subframepageid = (subframe >> 232) & 0b111111 + + elif sigcode in ( + "2L", + "2S", + "5I", + ): # QZSS L2C, L5I - CNAV + for i in range(numw): + wrd = getattr(data, f"dwrd_{i+1:02d}") + subframe += wrd << (32 * (numw - 1 - i)) + subframe = ( + (subframe >> 20) + & 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + ) # (2**300 - 1) + subframeid = (subframe >> 280) & 0b111111 + + elif sigcode == "1Z": # QZSS L1C - CNV2 + for i in range(numw): + wrd = getattr(data, f"dwrd_{i+1:02d}") + subframe += wrd << (32 * (numw - 1 - i)) + subframe = ( + subframe >> 12 + ) & 0x3FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF # (2**250 - 1) + subframeid = (subframe >> 236) & 0b111111 + + return { + "gnss": gnss, + "svid": svid, + "sigcode": sigcode, + "subframeid": subframeid, + "dataid": dataid, + "subframepageid": subframepageid, + "subframe": subframe, + } + + def _process_rxm_sfrbx_irn( + self, gnss: str, svid: int, sigcode: str, numw: int, data: UBXMessage + ) -> dict[str, str | int | float | NoneType]: + """ + Reassemble IRNSS (NAVIC) subframe from individual UBX RXM-SFRBX dwrds. + + - IRN L5A subframe = 292 bits; 10 * 32-bit dwrds with 28 bits padding + at end + + :param str gnss: RINEX gnss code e.g. "I" + :param int svid: SV + :param str sigcode: RINEX sigcode e.g. '5A' + :param UBXMessage data: parsed UBX RXM-SFRBX message + :return: dict of subframe attributes + :rtype: dict[str, str | int | float | NoneType] + :raises: RINEXProcessingError + """ + + subframe = 0 + subframeid = 0 + subframepageid = 0 + + if sigcode in ("5A",): # IRN L5A + for i in range(numw): + wrd = getattr(data, f"dwrd_{i+1:02d}") + subframe += wrd << (32 * (numw - 1 - i)) + subframe = ( + (subframe >> 28) + & 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + ) # (2**292 - 1) + subframeid = ((subframe >> 263) & 0b11) + 1 # remember to add 1 + if subframeid in (3, 4): + subframepageid = (subframe >> 256) & 0b111111 + + return { + "gnss": gnss, + "svid": svid, + "sigcode": sigcode, + "subframeid": subframeid, + "subframepageid": subframepageid, + "subframe": subframe, + } diff --git a/src/pygnssutils/rinex_subframes_bds.py b/src/pygnssutils/rawnav_subframes_bds.py similarity index 97% rename from src/pygnssutils/rinex_subframes_bds.py rename to src/pygnssutils/rawnav_subframes_bds.py index f08409b..f05b36b 100644 --- a/src/pygnssutils/rinex_subframes_bds.py +++ b/src/pygnssutils/rawnav_subframes_bds.py @@ -1,13 +1,11 @@ """ -rinex_subframes_bds.py +rawnav_subframes_bds.py Beidou NAV Subframe definitions. - -B1I: http://en.beidou.gov.cn/SYSTEMS/Officialdocument/201902/P020190227601370045731.pdf -B1C: http://en.beidou.gov.cn/SYSTEMS/Officialdocument/201806/P020180608525871869457.pdf -B2a: http://en.beidou.gov.cn/SYSTEMS/Officialdocument/201806/P020180608525870555377.pdf -B3I: http://en.beidou.gov.cn/SYSTEMS/Officialdocument/201806/P020180608525869304359.pdf -Open Service: http://en.beidou.gov.cn/SYSTEMS/ICD/201806/P020180608523308843290.pdf +B1C: http://www.beidou.gov.cn/xt/gfxz/201712/P020171226741342013031.pdf +B1I (D1/D2): http://en.beidou.gov.cn/SYSTEMS/ICD/201902/P020190227702348791891.pdf +B2a: http://www.beidou.gov.cn/xt/gfxz/201712/P020171226742357364174.pdf +B3I (D1/D2): http://www.beidou.gov.cn/xt/gfxz/201802/P020180209623601401189.pdf D1 is the BDS-2/3 legacy navigation message on MEO/IGSO satellites (obscode 2I, 6I, 7I). D2 is the BDS-2/3 legacy navigation message on GEO satellites (obscode 2I, 6I, 7I). @@ -492,6 +490,8 @@ BDS_CNV2_SUBFRAME_40 = {} # mapping for (subframe, page) acquisition mask subframeacq +# NB subframes containing only almanac data are not generally +# required for RINEX conversion purposes BDS_SUBFRAMEACQ_MAP = { D1: { TARGET: 0b1111, # subframes 1,2,3,5p10 diff --git a/src/pygnssutils/rinex_subframes_gal.py b/src/pygnssutils/rawnav_subframes_gal.py similarity index 98% rename from src/pygnssutils/rinex_subframes_gal.py rename to src/pygnssutils/rawnav_subframes_gal.py index 7f45f8b..1a8be74 100644 --- a/src/pygnssutils/rinex_subframes_gal.py +++ b/src/pygnssutils/rawnav_subframes_gal.py @@ -1,9 +1,9 @@ """ -rinex_subframes_gal.py +rawnav_subframes_gal.py Galileo NAV Subframe definitions. -https://galileognss.eu/wp-content/uploads/2021/01/Galileo_OS_SIS_ICD_v2.0.pdf +E1,E5a,E6: https://www.gsc-europa.eu/sites/default/files/sites/all/files/Galileo_OS_SIS_ICD_v2.1.pdf These are provided as the basis of a capability to parse and store the payloads of raw NAV subframe messages, via the associated @@ -420,6 +420,8 @@ } # mapping for (subframe, page) acquisition mask subframeacq +# NB subframes containing only almanac data are not generally +# required for RINEX conversion purposes GAL_SUBFRAMEACQ_MAP = { FNAV: { TARGET: 0b1111, # subframes 1,2,3,4 diff --git a/src/pygnssutils/rinex_subframes_glo.py b/src/pygnssutils/rawnav_subframes_glo.py similarity index 87% rename from src/pygnssutils/rinex_subframes_glo.py rename to src/pygnssutils/rawnav_subframes_glo.py index fac7324..e04df26 100644 --- a/src/pygnssutils/rinex_subframes_glo.py +++ b/src/pygnssutils/rawnav_subframes_glo.py @@ -1,9 +1,10 @@ """ -rinex_subframes_glo.py +rawnav_subframes_glo.py GLONASS NAV Subframe definitions. -https://web.archive.org/web/20161020203029/http://russianspacesystems.ru/wp-content/uploads/2016/08/ICD_GLONASS_eng_v5.1.pdf +L1OF,L2OF: http://gauss.gge.unb.ca/GLONASS.ICD.pdf +L1OF,L2OF: https://web.archive.org/web/20161020203029/http://russianspacesystems.ru/wp-content/uploads/2016/08/ICD_GLONASS_eng_v5.1.pdf These are provided as the basis of a capability to parse and store the payloads of raw NAV subframe messages, via the associated @@ -38,6 +39,10 @@ # L1OF (FDMA) - "1C" (same as L2OF - "2C") # ********************************************************************** +# TODO check that GLONASS is actually using two's complement for -ve values +# (ICD just refers to MSB being sign bit, without explicitly using the term +# two's complement) + # GLONASS ICD refers to subframes as 'strings' GLO_L1OF_SUBFRAME_TLM = { @@ -123,6 +128,8 @@ } # mapping for (subframe, page) acquisition mask subframeacq +# NB subframes containing only almanac data are not generally +# required for RINEX conversion purposes GLO_SUBFRAMEACQ_MAP = { L1OF: { TARGET: 0b11111, # subframes 1,2,3,4,5 diff --git a/src/pygnssutils/rinex_subframes_gps.py b/src/pygnssutils/rawnav_subframes_gps.py similarity index 97% rename from src/pygnssutils/rinex_subframes_gps.py rename to src/pygnssutils/rawnav_subframes_gps.py index 3e506a4..6c43d22 100644 --- a/src/pygnssutils/rinex_subframes_gps.py +++ b/src/pygnssutils/rawnav_subframes_gps.py @@ -1,9 +1,10 @@ """ -rinex_subframes_gps.py +rawnav_subframes_gps.py GPS NAV Subframe definitions. -https://archive.gps.gov/technical/icwg/IS-GPS-200N.pdf +L1C/A,L2CL,L2CM: https://www.gps.gov/sites/default/files/2025-07/IS-GPS-200N.pdf +L5I: https://www.gps.gov/sites/default/files/2025-07/IS-GPS-705J.pdf These are provided as the basis of a capability to parse and store the payloads of raw NAV subframe messages, via the associated @@ -22,8 +23,6 @@ :license: BSD 3-Clause """ -# pylint: disable=fixme - from pygnssutils.rawnav import PREAMBLE, SID, SPID, TOC, TOW, VALPREAMBLE, WN, S, U from pygnssutils.rinex_globals import ( CNAV, @@ -955,6 +954,8 @@ } # Integrity Support Message # mapping for (subframe, page) acquisition mask subframeacq +# NB subframes containing only almanac data are not generally +# required for RINEX conversion purposes GPS_SUBFRAMEACQ_MAP = { LNAV: { TARGET: 0b1111, # subframes 1,2,3,4p18 @@ -963,25 +964,14 @@ (2, 0): (GPS_LNAV_SUBFRAME_2, 2), (3, 0): (GPS_LNAV_SUBFRAME_3, 4), (4, 56): (GPS_LNAV_SUBFRAME_4_P18, 8), - (5, 0): (GPS_LNAV_SUBFRAME_45_GENERIC, 16), }, CNAV: { - TARGET: 0b1111, # subframes 10,11,30,33 + TARGET: 0b1111, # subframes 10,11,30,33 (32 EOP optional) START: 10, (10, 0): (GPS_CNAV_SUBFRAME_10, 1), (11, 0): (GPS_CNAV_SUBFRAME_11, 2), (30, 0): (GPS_CNAV_SUBFRAME_30, 4), (33, 0): (GPS_CNAV_SUBFRAME_33, 8), - (12, 0): (GPS_CNAV_SUBFRAME_12, 16), - (13, 0): (GPS_CNAV_SUBFRAME_13, 32), - (14, 0): (GPS_CNAV_SUBFRAME_14, 64), - (15, 0): (GPS_CNAV_SUBFRAME_15, 128), - (31, 0): (GPS_CNAV_SUBFRAME_31, 256), - (32, 0): (GPS_CNAV_SUBFRAME_32, 512), - (34, 0): (GPS_CNAV_SUBFRAME_34, 1024), - (35, 0): (GPS_CNAV_SUBFRAME_35, 2048), - (36, 0): (GPS_CNAV_SUBFRAME_36, 4096), - (37, 0): (GPS_CNAV_SUBFRAME_37, 8192), - (40, 0): (GPS_CNAV_SUBFRAME_40, 16384), + (32, 0): (GPS_CNAV_SUBFRAME_32, 16), }, } diff --git a/src/pygnssutils/rawnav_subframes_irn.py b/src/pygnssutils/rawnav_subframes_irn.py new file mode 100644 index 0000000..9ca4998 --- /dev/null +++ b/src/pygnssutils/rawnav_subframes_irn.py @@ -0,0 +1,171 @@ +""" +rawnav_subframes_irn.py + +IRNSS (NAVIC) NAV Subframe (Message Type) definitions. + +L5a: +https://www.isro.gov.in/media_isro/pdf/Publications/Vispdf/Pdf2017/irnss_sps_icd_version1.1-2017.pdf + +These are provided as the basis of a capability to parse and store +the payloads of raw NAV subframe messages, via the associated +`pygnssutils.RawNav` class defined in `rawnav.py`. + +NB: + +- MSB and LSB fields MUST be suffixed '_msb' and '_lsb' respectively. +- Non-data bits (reserved, parity, non) MUST be prefixed '_'. +- Avoid the following reserved field names: gnss, svid, sigid, subframeacq, epoch + +Created on 6 Oct 2025 + +:author: semuadmin (Steve Smith) +:copyright: semuadmin © 2025 +:license: BSD 3-Clause +""" + +from pygnssutils.rawnav import SID, SPID, TOW, WN, S, U +from pygnssutils.rinex_globals import ( + L1CA, + P2_N4, + P2_N19, + P2_N20, + P2_N21, + P2_N24, + P2_N25, + P2_N27, + P2_N28, + P2_N30, + P2_N31, + P2_N33, + P2_N35, + P2_N41, + P2_N43, + P2_N51, + P2_N55, + P2_N68, + P2_P4, + P2_P11, + P2_P14, + P2_P16, + START, + TARGET, +) + +# ********************************************************************** +# LNAV - "5A" +# ********************************************************************** + +IRN_LNAV_SUBFRAME_TLM = { + "_tlm": (0, 8, U, 0), + TOW: (8, 17, U, 12), + "alert": (25, 1, U, 0), + "autonav": (26, 1, U, 0), + SID: (27, 2, U, 0), + "_spare": (29, 1, U, 0), +} + +IRN_LNAV_SUBFRAME_END = {"_parity": (262, 24, U, 0), "_tail": (286, 6, U, 0)} + +IRN_LNAV_SUBFRAME_1 = { + **IRN_LNAV_SUBFRAME_TLM, + WN: (30, 10, U, 0), + "af0": (40, 22, S, P2_N31), + "af1": (62, 16, S, P2_N43), + "af2": (78, 8, S, P2_N55), + "ura": (86, 4, U, 0), + "toc": (90, 16, U, 16), + "tgd": (106, 8, S, P2_N31), + "deltan": (114, 22, S, P2_N41), + "iodec": (136, 8, U, 1), + "_reserved1": (144, 10, U, 0), + "l5flag": (154, 1, U, 0), + "sflag": (155, 1, U, 0), + "cuc": (156, 15, S, P2_N28), + "cus": (171, 15, S, P2_N28), + "cic": (186, 15, S, P2_N28), + "cis": (201, 15, S, P2_N28), + "crc": (216, 15, S, P2_N4), + "crs": (231, 15, S, P2_N4), + "idot": (246, 14, S, P2_N43), + "_reserved2": (260, 2, U, 0), + **IRN_LNAV_SUBFRAME_END, +} + +IRN_LNAV_SUBFRAME_2 = { + **IRN_LNAV_SUBFRAME_TLM, + "m0": (30, 32, S, P2_N31), + "toe": (62, 16, U, 16), + "e": (78, 32, U, P2_N33), + "sqrta": (110, 32, U, P2_N19), + "omega0": (142, 32, S, P2_N31), + "omega": (174, 32, S, P2_N31), + "omegadot": (206, 22, S, P2_N41), + "i0": (228, 32, S, P2_N31), + "_reserved1": (260, 2, U, 0), + **IRN_LNAV_SUBFRAME_END, +} + +IRN_LNAV_SUBFRAME_3_P9 = { + **IRN_LNAV_SUBFRAME_TLM, + SPID: (30, 6, U, 0), + "a0utc": (36, 16, S, P2_N35), + "a1utc": (52, 13, S, P2_N51), + "a2utc": (65, 7, S, P2_N68), + "deltatls": (72, 8, S, 1), + "toutc": (80, 16, U, P2_P4), + "wnoutc": (96, 10, U, 1), + "wnlsf": (106, 10, U, 1), + "dn": (116, 4, U, 1), + "deltatlsf": (120, 8, S, 1), + "a0": (128, 16, S, P2_N35), + "a1": (144, 13, S, P2_N51), + "a2": (157, 7, S, P2_N68), + "tot": (164, 16, U, P2_P4), + "wnot": (180, 10, U, 1), + "gnssid": (190, 3, U, 0), + "_reserved1": (193, 63, U, 0), + "prn": (256, 6, U, 0), + **IRN_LNAV_SUBFRAME_END, +} + +IRN_LNAV_SUBFRAME_3_P11 = { + **IRN_LNAV_SUBFRAME_TLM, + SPID: (30, 6, U, 0), + "teop": (36, 16, S, P2_P4), + "pmx": (52, 21, S, P2_N20), + "pmxdot": (73, 15, S, P2_N21), + "pmy": (88, 21, S, P2_N20), + "pmydot": (109, 15, S, P2_N21), + "deltaut1": (124, 31, S, P2_N24), + "deltautdot1": (155, 19, S, P2_N25), + "alpha0": (174, 8, S, P2_N30), + "alpha1": (182, 8, S, P2_N27), + "alpha2": (190, 8, S, P2_N24), + "alpha3": (198, 8, S, P2_N24), + "beta0": (206, 8, S, P2_P11), + "beta1": (214, 8, S, P2_P14), + "beta2": (222, 8, S, P2_P16), + "beta3": (230, 8, S, P2_P16), + "_reserved1": (238, 18, U, 0), + "prn": (256, 6, U, 0), + **IRN_LNAV_SUBFRAME_END, +} + +IRN_LNAV_SUBFRAME_4_P9 = IRN_LNAV_SUBFRAME_3_P9 +IRN_LNAV_SUBFRAME_4_P11 = IRN_LNAV_SUBFRAME_3_P11 + +# mapping for (subframe, page) acquisition mask subframeacq +# NB subframes containing only almanac data are not generally +# required for RINEX conversion purposes +IRN_SUBFRAMEACQ_MAP = { + L1CA: { + TARGET: 0b1111, # Subframes 1, 2, 3/4P9, 3/4P11 + START: 1, + (1, 0): (IRN_LNAV_SUBFRAME_1, 1), + (2, 0): (IRN_LNAV_SUBFRAME_2, 2), + (3, 9): (IRN_LNAV_SUBFRAME_3_P9, 4), + (4, 9): (IRN_LNAV_SUBFRAME_4_P9, 4), + (3, 11): (IRN_LNAV_SUBFRAME_3_P11, 8), + (4, 11): (IRN_LNAV_SUBFRAME_4_P11, 8), + }, +} diff --git a/src/pygnssutils/rawnav_subframes_qzs.py b/src/pygnssutils/rawnav_subframes_qzs.py new file mode 100644 index 0000000..a7a2a76 --- /dev/null +++ b/src/pygnssutils/rawnav_subframes_qzs.py @@ -0,0 +1,518 @@ +""" +rawnav_subframes_qzss.py + +QZSS NAV Subframe (Message Type) definitions. + +L1C/A,L2CM, L2CL, L5I: +https://qzss.go.jp/en/technical/download/pdf/ps-is-qzss/is-qzss-pnt-006.pdf?t=1782205084546 +L1S: https://qzss.go.jp/en/technical/ps-is-qzss/is_qzss_l1s_007_agree.html + +These are provided as the basis of a capability to parse and store +the payloads of raw NAV subframe messages, via the associated +`pygnssutils.RawNav` class defined in `rawnav.py`. + +NB: + +- MSB and LSB fields MUST be suffixed '_msb' and '_lsb' respectively. +- Non-data bits (reserved, parity, non) MUST be prefixed '_'. +- Avoid the following reserved field names: gnss, svid, sigid, subframeacq, epoch + +Created on 6 Oct 2025 + +:author: semuadmin (Steve Smith) +:copyright: semuadmin © 2025 +:license: BSD 3-Clause +""" + +from pygnssutils.rawnav import PREAMBLE, SID, SPID, TOC, TOW, WN, S, U +from pygnssutils.rinex_globals import ( + CNAV, + CNV2, + LNAV, + P2_N5, + P2_N8, + P2_N9, + P2_N19, + P2_N20, + P2_N21, + P2_N23, + P2_N24, + P2_N25, + P2_N27, + P2_N29, + P2_N30, + P2_N31, + P2_N32, + P2_N33, + P2_N34, + P2_N35, + P2_N43, + P2_N44, + P2_N48, + P2_N50, + P2_N51, + P2_N55, + P2_N57, + P2_N60, + P2_N68, + P2_P4, + P2_P11, + P2_P12, + P2_P14, + P2_P16, + START, + TARGET, +) + +# ********************************************************************** +# LNAV - "1C" (L1C/A, L1C/B) - SAME AS GPS LNAV +# ********************************************************************** + +# attribute_name: (bit offset, bit length, bit encoding, scaling) +QZS_LNAV_TLM = { + PREAMBLE: (0, 8, U, 0), + "tlm": (8, 14, U, 0), + "integrity": (22, 1, U, 0), + "_reserved1": (23, 1, U, 0), + "_parity1": (24, 6, U, 0), +} + +QZS_LNAV_HOW = { + TOW: (30, 17, U, 6), # used to derive epoch, check TOW * 6 = seconds + "alert": (47, 1, U, 0), + "antispoof": (48, 1, U, 0), + SID: (49, 3, U, 0), # subframe id + "_non1": (52, 2, U, 0), + "_parity2": (54, 6, U, 0), +} + +QZS_LNAV_GENERIC = { + # word1 + **QZS_LNAV_TLM, + # word2 + **QZS_LNAV_HOW, + # words 3 - 10 + "_word3_10": (60, 240, U, 0), +} + +QZS_LNAV_SUBFRAME_1 = { + # word1 + **QZS_LNAV_TLM, + # word2 + **QZS_LNAV_HOW, + # word3 + WN: (60, 10, U, 1), # used to derive epoch + "l2codes": (70, 2, U, 1), + "ura": (72, 4, U, 1), + "svhealth": (76, 6, U, 1), + # "l1health": (76,1,U,0), + # "l1cahealth": (77,1,U,0), + # "l2health": (78,1,U,0), + # "l5health": (79,1,U,0), + # "l1chealth": (80,1,U,0), + # "l1cbhealth": (81,1,U,0), + "iodc_msb": (82, 2, U, 0), + "_parity3": (84, 6, U, 0), + # word4 + "l2pdata": (90, 1, U, 1), + "_reserved2": (91, 23, U, 0), + "_parity4": (114, 6, U, 0), + # word5 + "_reserved3": (120, 24, U, 0), + "_parity5": (144, 6, U, 0), + # word6 + "_reserved4": (150, 24, U, 0), + "_parity6": (174, 6, U, 0), + # word7 + "_reserved5": (180, 16, U, 0), + "tgd": (196, 8, S, P2_N31), + "_parity7": (204, 6, U, 0), + # word8 + "iodc_lsb": (210, 8, U, 0), + TOC: (218, 16, U, P2_P4), # must be named "toc" + "_parity8": (234, 6, U, 0), + # word9 + "af2": (240, 8, S, P2_N55), + "af1": (248, 16, S, P2_N43), + "_parity9": (264, 6, U, 0), + # word10 + "af0": (270, 22, S, P2_N31), + "_non2": (292, 2, U, 0), + "_parity10": (294, 6, U, 0), +} + +QZS_LNAV_SUBFRAME_2 = { + # word1 + **QZS_LNAV_TLM, + # word2 + **QZS_LNAV_HOW, + # word3 + "iode": (60, 8, U, 0), + "crs": (68, 16, S, P2_N5), + "_parity3": (84, 6, U, 0), + # word4 + "deltan": (90, 16, S, P2_N43), + "m0_msb": (106, 8, S, P2_N31), + "_parity4": (114, 6, U, 0), + # word5 + "m0_lsb": (120, 24, S, P2_N31), + "_parity5": (144, 6, U, 0), + # word6 + "cuc": (150, 16, S, P2_N29), + "e_msb": (166, 8, U, 0), + "_parity6": (174, 6, U, 0), + # word7 + "e_lsb": (180, 24, U, P2_N33), + "_parity7": (204, 6, U, 0), + # word8 + "cus": (210, 16, S, P2_N29), + "sqrta_msb": (226, 8, U, P2_N19), + "_parity8": (234, 6, U, 0), + # word9 + "sqrta_lsb": (240, 24, U, P2_N19), + "_parity9": (264, 6, U, 0), + # word10 + "toe": (270, 16, U, P2_P4), + "fit": (286, 1, U, 0), + "aodo": (287, 5, U, 900), + "_non2": (292, 2, U, 0), + "_parity10": (294, 6, U, 0), +} + +QZS_LNAV_SUBFRAME_3 = { + # word1 + **QZS_LNAV_TLM, + # word2 + **QZS_LNAV_HOW, + # word3 + "cic": (60, 16, S, P2_N29), + "omega0_msb": (76, 8, S, P2_N31), + "_parity3": (84, 6, U, 0), + # word4 + "omega0_lsb": (90, 24, S, P2_N31), + "_parity4": (114, 6, U, 0), + # word5 + "cis": (120, 16, S, P2_N29), + "i0_msb": (136, 8, S, P2_N31), + "_parity5": (144, 6, U, 0), + # word6 + "i0_lsb": (150, 24, S, P2_N31), + "_parity6": (174, 6, U, 0), + # word7 + "crc": (180, 16, S, P2_N5), + "omega_msb": (196, 8, S, P2_N31), + "_parity7": (204, 6, U, 0), + # word8 + "omega_lsb": (210, 24, S, P2_N31), + "_parity8": (234, 6, U, 0), + # word9 + "omegadot": (240, 24, S, P2_N43), + "_parity9": (264, 6, U, 0), + # word10 + "iode": (270, 8, U, 0), + "idot": (278, 14, S, P2_N43), + "_non2": (292, 2, U, 0), + "_parity10": (294, 6, U, 0), +} + +QZS_LNAV_SUBFRAME_4_P56 = { + # word1 + **QZS_LNAV_TLM, + # word2 + **QZS_LNAV_HOW, + # word3 + "dataid": (60, 2, U, 0), # = 3 + SPID: (62, 6, U, 0), # = 56 + "alpha0": (68, 8, S, P2_N30), + "alpha1": (76, 8, S, P2_N27), + "_parity3": (84, 6, U, 0), + # word4 + "alpha2": (90, 8, S, P2_N24), + "alpha3": (98, 8, S, P2_N24), + "beta0": (106, 8, S, P2_P11), + "_parity4": (114, 6, U, 0), + # word5 + "beta1": (120, 8, S, P2_P14), + "beta2": (128, 8, S, P2_P16), + "beta3": (136, 8, S, P2_P16), + "_parity5": (144, 6, U, 0), + # word6 + "a1": (150, 24, S, P2_N30), + "_parity6": (174, 6, U, 0), + # word7 + "a0_msb": (180, 24, S, P2_N50), + "_parity7": (204, 6, U, 0), + # word8 + "a0_lsb": (210, 8, S, P2_N50), + "tot": (218, 8, U, P2_P12), + "wnt": (226, 8, U, 0), + "_parity8": (234, 6, U, 0), + # word9 + "deltatls": (240, 8, S, 1), + "wnlsf": (248, 8, U, 0), + "dn": (256, 8, U, 0), + "_parity9": (264, 6, U, 0), + # word10 + "deltatlsf": (270, 8, S, 1), + "_reserved2": (278, 14, U, 0), + "_non2": (292, 2, U, 0), + "_parity10": (294, 6, U, 0), +} + +QZS_LNAV_SUBFRAME_5_P56 = QZS_LNAV_SUBFRAME_4_P56 + +# ********************************************************************** +# CNAV - "2S","2L","5I","5Q" (L2C, L5) - SAME AS GPS CNAV +# ********************************************************************** + +QZS_CNAV_TLM = { + # VALPREAMBLE: 0b10001011, # optional, used to validate preamble value + PREAMBLE: (0, 8, U, 0), + "prn": (8, 6, U, 0), + SID: (14, 6, U, 0), + TOW: (20, 17, U, 6), # used to derive epoch, check TOW * 6 = seconds + "alert": (37, 1, U, 0), +} + +QZS_CNAV_CLOCK = { + "top": (38, 11, U, 300), + "uraned0": (49, 5, S, 0), + "uraned1": (54, 3, U, 0), + "uraned2": (57, 3, U, 0), + TOC: (60, 11, U, 300), + "af0n": (71, 26, S, P2_N35), + "af1n_msb": (97, 3, S, P2_N48), + "af1n_lsb": (100, 17, S, P2_N48), + "af2n": (117, 10, S, P2_N60), +} + +QZS_CNAV_PARITY = { + "_parity": (276, 24, U, 0), +} + +QZS_CNAV_SUBFRAME_10 = { + **QZS_CNAV_TLM, + WN: (38, 13, U, 0), # used to derive epoch + "l1health": (51, 1, U, 0), + "l2health": (52, 1, U, 0), + "l5health": (53, 1, U, 0), + "top": (54, 11, U, 300), + "uraed": (65, 5, U, 0), + "toe": (70, 11, U, 300), + "deltaa_msb": (81, 19, S, P2_N9), + "deltaa_lsb": (100, 7, S, P2_N9), + "adot": (107, 25, S, P2_N21), + "deltan0": (132, 17, S, P2_N44), + "deltan0dot": (149, 23, S, P2_N57), + "m0_msb": (172, 28, S, P2_N32), + "m0_lsb": (200, 5, S, P2_N32), + "e": (205, 33, U, P2_N34), + "omega": (238, 33, S, P2_N32), + "integrity": (271, 1, U, 0), + "l2phase": (272, 1, U, 0), + "_reserved1": (273, 3, U, 0), + **QZS_CNAV_PARITY, +} # Ephemeris 1 + +QZS_CNAV_SUBFRAME_11 = { + **QZS_CNAV_TLM, + "toe": (38, 11, U, 300), + "omega0": (49, 33, S, P2_N32), + "i0_msb": (82, 18, S, P2_N32), + "i0_lsb": (100, 15, S, P2_N32), + "deltaomegadot": (115, 17, S, P2_N44), + "idot": (132, 15, S, P2_N44), + "cis": (147, 16, S, P2_N30), + "cic": (163, 16, S, P2_N30), + "crs_msb": (179, 21, S, P2_N8), + "crs_lsb": (200, 3, S, P2_N8), + "crc": (203, 24, S, P2_N8), + "cus": (227, 21, S, P2_N30), + "cuc": (248, 21, S, P2_N30), + "_reserved1": (269, 7, U, 0), + **QZS_CNAV_PARITY, +} # Ephemeris 2 + +QZS_CNAV_SUBFRAME_30 = { + **QZS_CNAV_TLM, + **QZS_CNAV_CLOCK, + "tgd": (127, 13, S, P2_N35), + "iscl1ca": (140, 13, S, P2_N35), + "iscl2c": (153, 13, S, P2_N35), + "iscl5i5": (166, 13, S, P2_N35), + "iscl5q5": (179, 13, S, P2_N35), + "alpha0": (192, 8, U, P2_N30), # where is scaling defined for CNAV? + "alpha1": (200, 8, U, P2_N27), # have assumed same as LNAV + "alpha2": (208, 8, U, P2_N24), + "alpha3": (216, 8, U, P2_N24), + "beta0": (224, 8, U, P2_P11), + "beta1": (232, 8, U, P2_P14), + "beta2": (240, 8, U, P2_P16), + "beta3": (248, 8, U, P2_P16), + "wno": (256, 8, U, 0), + "_reserved1": (264, 12, U, 0), + **QZS_CNAV_PARITY, +} # Clock, IONO & Group Delay + +QZS_CNAV_SUBFRAME_32 = { + **QZS_CNAV_TLM, + **QZS_CNAV_CLOCK, + "teop": (127, 16, U, P2_P4), + "pmx": (143, 21, S, P2_N20), + "pmxdot": (164, 15, S, P2_N21), + "pmy": (179, 21, S, P2_N20), + "pmydot": (200, 15, S, P2_N21), + "deltautgps": (215, 31, S, P2_N23), + "deltautgpsdot": (246, 19, S, P2_N25), + "_reserved1": (265, 11, U, 0), + **QZS_CNAV_PARITY, +} # Clock & EOP + +QZS_CNAV_SUBFRAME_33 = { + **QZS_CNAV_TLM, + **QZS_CNAV_CLOCK, + "a0": (127, 16, S, P2_N35), + "a1": (143, 13, S, P2_N51), + "a2": (156, 7, S, P2_N68), + "deltatls": (163, 8, S, 1), + "tot": (171, 16, U, P2_P4), + "wnot": (187, 13, U, 1), + "wnlsf": (200, 13, U, 1), + "dn": (213, 4, U, 1), + "deltatlsf": (217, 8, S, 1), + "_reserved1": (225, 51, U, 0), + **QZS_CNAV_PARITY, +} # Clock & UTC + +# ********************************************************************** +# CNV2 - "1S" (L1S) +# ********************************************************************** + +QZS_CNV2_SUBFRAME_1 = {"toi": (0, 9, U, 18)} + +QZS_CNV2_SUBFRAME_2 = { + WN: (0, 13, U, 1), + "itow": (13, 8, U, 1), + "top": (21, 11, U, 300), + "l1chealth": (32, 1, U, 1), + "uraed": (33, 5, S, 1), + "toe": (38, 11, U, 300), + "deltaa": (49, 26, S, P2_N9), + "adot": (75, 25, S, P2_N21), + "deltan0": (100, 17, S, P2_N44), + "deltan0dot": (117, 23, S, P2_N57), + "m0_msb": (140, 10, S, P2_N32), + "m0_lsb": (150, 23, S, P2_N32), + "e": (173, 33, S, P2_N34), + "omega": (206, 33, S, P2_N32), + "omega0": (239, 33, S, P2_N32), + "i0_msb": (272, 28, S, P2_N32), + "i0_lsb": (300, 5, S, P2_N32), + "deltanomegadot": (305, 17, S, P2_N44), + "idot": (322, 15, S, P2_N44), + "cis": (337, 16, S, P2_N30), + "cic": (353, 16, S, P2_N30), + "crs": (369, 24, S, P2_N8), + "crc": (393, 24, S, P2_N8), + "cus": (417, 21, S, P2_N30), + "cuc_msb": (438, 12, S, P2_N30), + "cuc_lsb": (450, 9, S, P2_N30), + "uraned0": (459, 5, S, 1), + "uraned1": (464, 3, U, 1), + "uraned2": (467, 3, U, 1), + "af0": (470, 26, S, P2_N35), + "af1": (496, 20, S, P2_N48), + "af2": (516, 10, S, P2_N60), + "tgd": (526, 13, S, P2_N35), + "iscl1gp": (539, 13, S, P2_N35), + "iscl1cd": (552, 13, S, P2_N35), + "isf": (565, 1, S, 0), + "wnop": (566, 8, S, 1), + "_reserved": (574, 2, U, 0), + "_crc": (576, 24, U, 0), +} # emphemeris + +QZS_CNV2_SUBFRAME_3_P1 = { + "prn": (0, 8, U, 0), + SPID: (8, 6, U, 0), # = 1 + "a0": (14, 16, S, P2_N35), + "a1": (30, 13, S, P2_N51), + "a2": (43, 7, S, P2_N68), + "deltatls": (50, 8, S, 1), + "tot": (58, 16, U, P2_P4), + "wnot": (74, 13, U, 1), + "wnlsf": (87, 13, U, 1), + "dn": (100, 4, U, 1), + "deltatlsf": (104, 8, S, 1), + "alpha0": (112, 8, S, P2_N30), + "alpha1": (120, 8, S, P2_N27), + "alpha2": (128, 8, S, P2_N24), + "alpha3": (136, 8, S, P2_N24), + "beta0_msb": (144, 6, S, P2_P11), + "beta0_lsb": (150, 2, S, P2_P11), + "beta1": (152, 8, S, P2_P14), + "beta2": (160, 8, S, P2_P16), + "beta3": (168, 8, S, P2_P16), + "iscl1": (176, 13, S, P2_N35), + "lscl2": (189, 13, S, P2_N35), + "iscl5i5": (202, 13, S, P2_N35), + "iscl5q5": (215, 13, S, P2_N35), + "_reserved1": (228, 22, U, 0), + "_crc": (250, 24, U, 0), +} # UTC and iono parameters (wide area) + +QZS_CNV2_SUBFRAME_3_P2 = { + "prn": (0, 8, U, 0), + SPID: (8, 6, U, 0), # = 2 + "gnssid": (14, 3, U, 0), + "tggto": (17, 16, U, P2_P4), + "wnggto": (33, 13, U, 1), + "a0ggto": (46, 16, S, P2_N35), + "a1ggto": (62, 13, S, P2_N51), + "a2ggto": (75, 7, S, P2_N68), + "teop": (82, 16, U, P2_P4), + "pmx": (98, 21, S, P2_N20), + "pmxdot": (119, 15, S, P2_N21), + "pmy_msb": (134, 16, S, P2_N20), + "pmy_lsb": (150, 5, S, P2_N20), + "pmydot": (155, 15, S, P2_N21), + "deltaut1": (170, 31, S, P2_N24), + "deltaut1dot": (201, 19, S, P2_N25), + "_reserved": (220, 30, U, 0), + "_crc": (250, 24, U, 0), +} # GGTO and EOP + +QZS_CNV2_SUBFRAME_3_P61 = QZS_CNV2_SUBFRAME_3_P1 # UTC and IONO parameters (Japan area) + +# mapping for (subframe, page) acquisition mask subframeacq +# NB subframes containing only almanac data are not generally +# required for RINEX conversion purposes +QZS_SUBFRAMEACQ_MAP = { + LNAV: { + TARGET: 0b1111, # subframes 1,2,3,4p56 + START: 1, + (1, 0): (QZS_LNAV_SUBFRAME_1, 1), + (2, 0): (QZS_LNAV_SUBFRAME_2, 2), + (3, 0): (QZS_LNAV_SUBFRAME_3, 4), + (4, 56): (QZS_LNAV_SUBFRAME_4_P56, 8), + (5, 56): (QZS_LNAV_SUBFRAME_5_P56, 8), + }, + CNAV: { + TARGET: 0b1111, # subframes 10,11,30,33 (32 EOP optional) + START: 10, + (10, 0): (QZS_CNAV_SUBFRAME_10, 1), + (11, 0): (QZS_CNAV_SUBFRAME_11, 2), + (30, 0): (QZS_CNAV_SUBFRAME_30, 4), + (33, 0): (QZS_CNAV_SUBFRAME_33, 8), + (32, 0): (QZS_CNAV_SUBFRAME_32, 16), + }, + CNV2: { + TARGET: 0b1111, # subframes 1,2,3P1,3P2 (3P61 for JAPAN) + START: 1, + (1, 0): (QZS_CNV2_SUBFRAME_1, 1), + (2, 0): (QZS_CNV2_SUBFRAME_2, 2), + (3, 1): (QZS_CNV2_SUBFRAME_3_P1, 4), + (3, 2): (QZS_CNV2_SUBFRAME_3_P2, 8), + # (3, 61): (QZS_CNV2_SUBFRAME_3_P61, 16), + }, +} diff --git a/src/pygnssutils/rawnav_subframes_sba.py b/src/pygnssutils/rawnav_subframes_sba.py new file mode 100644 index 0000000..e8a1e64 --- /dev/null +++ b/src/pygnssutils/rawnav_subframes_sba.py @@ -0,0 +1,144 @@ +""" +rawnav_subframes_sba.py + +SBAS NAV Subframe (Message Type) definitions. + +ICAO Standards and Recommended Practices (SARPS) Annex10 Volume I (Radio Navigation Aids) +https://www.bazl.admin.ch/dam/en/sd-web/8uK1mTR6IvZh/AN10_V1_cons.pdf +L1C/A: https://gnss-x.ac.cn/docs/RTCA-DO-229D.pdf +A.4.4.11 GEO Navigation Message Type 9 +A.4.4.12 ALMANAC Navigation Message Type 17 +A.4.4.15 Network Time/UTC/GLONASS Time Offset Parameters Message Type 12 + +These are provided as the basis of a capability to parse and store +the payloads of raw NAV subframe messages, via the associated +`pygnssutils.RawNav` class defined in `rawnav.py`. + +NB: + +- MSB and LSB fields MUST be suffixed '_msb' and '_lsb' respectively. +- Non-data bits (reserved, parity, non) MUST be prefixed '_'. +- Avoid the following reserved field names: gnss, svid, sigid, subframeacq, epoch + +Created on 6 Oct 2025 + +:author: semuadmin (Steve Smith) +:copyright: semuadmin © 2025 +:license: BSD 3-Clause +""" + +from pygnssutils.rawnav import PREAMBLE, SID, TOW, WN, S, U +from pygnssutils.rinex_globals import ( + L1CA, + P2_N30, + P2_N31, + P2_N40, + P2_N50, + P2_P12, + START, + TARGET, +) + +# ********************************************************************** +# L1CA - "1C" +# ********************************************************************** + +# SBAS ICD (RTCA-DO-229D) refers to subframes as 'Message Types' (MT) + +SBA_L1CA_MT_TLM = { + PREAMBLE: (0, 8, U, 0), + SID: (8, 6, U, 0), # MT record number +} + +SBA_L1CA_MT_END = { + "_parity": (226, 24, U, 0), +} + +# attribute_name: (bit offset, bit length, bit encoding, scaling) +SBA_L1CA_MT_9 = { + **SBA_L1CA_MT_TLM, + "iodn": (14, 8, U, 0), # marked as reserved + "t0": (22, 13, U, 16), + "ura": (35, 4, U, 0), # 15 = do not use for ranging + "xpos": (39, 30, S, 0.08), + "ypos": (69, 30, S, 0.08), + "zpos": (99, 25, S, 0.4), + "xdot": (124, 17, S, 0.000625), + "ydot": (141, 17, S, 0.000625), + "zdot": (158, 18, S, 0.004), + "xdot2": (176, 10, S, 0.0000125), + "ydot2": (186, 10, S, 0.0000125), + "zdot2": (196, 10, S, 0.0000625), + "agf0": (206, 12, S, P2_N31), + "agf1": (218, 8, S, P2_N40), + **SBA_L1CA_MT_END, +} + +SBA_L1CA_MT_12 = { + **SBA_L1CA_MT_TLM, + "a1": (14, 24, S, P2_N50), + "a0": (38, 32, S, P2_N30), + "toc": (70, 8, U, P2_P12), # tot + "wnt": (78, 8, U, 1), + "deltatls": (86, 8, S, 1), + "wnlsf": (94, 8, U, 1), + "dn": (102, 8, U, 1), + "deltatlsf": (110, 8, S, 1), + "utcid": (118, 3, U, 1), # see UTCID lookup + TOW: (121, 20, U, 1), + WN: (141, 10, U, 1), + "gloind": (151, 1, U, 0), + "deltatglo": (152, 74, U, 0), + **SBA_L1CA_MT_END, +} + +SBA_L1CA_MT_17 = { + **SBA_L1CA_MT_TLM, + "dataid_01": (14, 2, U, 0), + "prn_01": (16, 8, U, 0), + "svhealth_01": (24, 8, U, 0), + # Bit 0 (LSB) Ranging On (0), Off (1); 1 = do not use for ranging + # Bit 1 Corrections On (0), Off (1); 1 = do not use for corrections + # Bit 2 Broadcast Integrity On (0), Off (1) + # Bits 3 Reserved + # Bits 4-7 Service Provider ID + "xg_01": (32, 15, S, 2600), + "yg_01": (47, 15, S, 2600), + "zg_01": (62, 9, S, 26000), + "xgdot_01": (71, 3, S, 10), + "ygdot_01": (74, 3, S, 10), + "zgdot_01": (77, 4, S, 60), + "dataid_02": (81, 2, U, 0), + "prn_02": (83, 8, U, 0), + "svhealth_02": (91, 8, U, 0), + "xg_02": (99, 15, S, 2600), + "yg_02": (114, 15, S, 2600), + "zg_02": (129, 9, S, 26000), + "xgdot_02": (138, 3, S, 10), + "ygdot_02": (141, 3, S, 10), + "zgdot_02": (144, 4, S, 60), + "dataid_03": (148, 2, U, 0), + "prn_03": (150, 8, U, 0), + "svhealth_03": (158, 8, U, 0), + "xg_03": (166, 15, S, 2600), + "yg_03": (181, 15, S, 2600), + "zg_03": (196, 9, S, 26000), + "xgdot_03": (205, 3, S, 10), + "ygdot_03": (208, 3, S, 10), + "zgdot_03": (211, 4, S, 60), + "t0": (215, 11, S, 64), + **SBA_L1CA_MT_END, +} + +# mapping for (subframe, page) acquisition mask subframeacq +# NB subframes containing only almanac data are not generally +# required for RINEX conversion purposes +SBA_SUBFRAMEACQ_MAP = { + L1CA: { + TARGET: 0b111, # Message Type (MT) codes 9,12 (17 optional) + START: 9, + (9, 0): (SBA_L1CA_MT_9, 1), + (12, 0): (SBA_L1CA_MT_12, 2), + (17, 0): (SBA_L1CA_MT_17, 4), + }, +} diff --git a/src/pygnssutils/rinex_conv.py b/src/pygnssutils/rinex_conv.py index 7225548..d61788a 100644 --- a/src/pygnssutils/rinex_conv.py +++ b/src/pygnssutils/rinex_conv.py @@ -3,10 +3,8 @@ RINEX Conversion Common class. -A preliminary implementation of a RINEX conversion utility for -observation, navigation and meteorological data. - -Functionality will be extended in future versions - contributions welcome. +A RINEX conversion utility for observation, navigation and +meteorological data. Created on 6 Oct 2025 diff --git a/src/pygnssutils/rinex_conv_met.py b/src/pygnssutils/rinex_conv_met.py index 6deb3db..a662007 100644 --- a/src/pygnssutils/rinex_conv_met.py +++ b/src/pygnssutils/rinex_conv_met.py @@ -3,15 +3,11 @@ RINEX Conversion Meterology class. -A preliminary implementation of a RINEX meteorology conversion utility. - Converts NMEA MWD and XDR messages to RINEX Meteorology text format. Meteorology data comprises meteorological sensor readings such as temperature, pressure, wind speed and direction, rain levels, etc. -Functionality may be extended in future versions - contributions welcome. - Created on 6 Oct 2025 :author: semuadmin (Steve Smith) diff --git a/src/pygnssutils/rinex_conv_nav.py b/src/pygnssutils/rinex_conv_nav.py index a3d5be8..aa709cc 100644 --- a/src/pygnssutils/rinex_conv_nav.py +++ b/src/pygnssutils/rinex_conv_nav.py @@ -5,12 +5,6 @@ Converts NAV message data to RINEX Navigation text format. -NB: Alpha release currently limited to following data sources: - -- RawNav objects containing data collated from UBX RXM-SFRBX messages -- RTCM3 ephemerides messages 1019, 1020, 1041-1046 e.g. from RTK receiver - or NTRIP data stream - Created on 6 Oct 2025 :author: semuadmin (Steve Smith) @@ -32,12 +26,21 @@ from pygnssutils.exceptions import RINEXProcessingError from pygnssutils.globals import VERBOSITY_MEDIUM -from pygnssutils.rawnav import RawNav, RawNavReader +from pygnssutils.rawnav import RawNav +from pygnssutils.rawnav_reader import RawNavReader +from pygnssutils.rawnav_subframes_bds import BDS_SUBFRAMEACQ_MAP +from pygnssutils.rawnav_subframes_gal import GAL_SUBFRAMEACQ_MAP +from pygnssutils.rawnav_subframes_glo import GLO_SUBFRAMEACQ_MAP +from pygnssutils.rawnav_subframes_gps import GPS_SUBFRAMEACQ_MAP +from pygnssutils.rawnav_subframes_irn import IRN_SUBFRAMEACQ_MAP +from pygnssutils.rawnav_subframes_qzs import QZS_SUBFRAMEACQ_MAP +from pygnssutils.rawnav_subframes_sba import SBA_SUBFRAMEACQ_MAP from pygnssutils.rinex_globals import ( AREF, BDS, BOD, CNAV, + CNV2, D1, D2, EOP, @@ -51,6 +54,7 @@ ION, IRN, KLOB, + L1CA, L1OF, LNAV, NAV, @@ -59,9 +63,12 @@ QZS, RINEX4, RINEXGNSSR, + SBA, + SBASPRN, START, STO, TARGET, + UTCID, ) from pygnssutils.rinex_helpers import ( # format_timefirstlast, DRNX, @@ -81,10 +88,6 @@ glotk2sec, gpsura2m, ) -from pygnssutils.rinex_subframes_bds import BDS_SUBFRAMEACQ_MAP -from pygnssutils.rinex_subframes_gal import GAL_SUBFRAMEACQ_MAP -from pygnssutils.rinex_subframes_glo import GLO_SUBFRAMEACQ_MAP -from pygnssutils.rinex_subframes_gps import GPS_SUBFRAMEACQ_MAP CLKBIAS = "clkbias" CLKDRIFT = "clkdrift" @@ -276,7 +279,25 @@ def _collate_rxmsfrbx(self, data: UBXMessage): sfrmap = GLO_SUBFRAMEACQ_MAP[L1OF] formatter = self._format_rawnav_glo_l1of kwargs = {"freqid": sfrdata.get("freqid", 0)} - # elif other gnss/sigcode, as and when I get to it TODO + elif gnss == SBA: + if sigcode in ("1C",): + sfrmap = SBA_SUBFRAMEACQ_MAP[L1CA] + formatter = self._format_rawnav_sba_l1ca + kwargs = {"freqid": sfrdata.get("freqid", 0)} + elif gnss == QZS: + if sigcode in ("1C",): + sfrmap = QZS_SUBFRAMEACQ_MAP[LNAV] + formatter = self._format_rawnav_qzs_lnav + elif sigcode in ("2S", "2L", "5S", "5Q"): + sfrmap = QZS_SUBFRAMEACQ_MAP[CNAV] + formatter = self._format_rawnav_qzs_cnav + elif sigcode in ("1Z",): + sfrmap = QZS_SUBFRAMEACQ_MAP[CNV2] + formatter = self._format_rawnav_qzs_cnv2 + elif gnss == IRN: + if sigcode in ("5A",): + sfrmap = IRN_SUBFRAMEACQ_MAP[LNAV] + formatter = self._format_rawnav_irn_lnav if sfrmap is None or formatter is None: return @@ -1063,7 +1084,7 @@ def _format_rawnav_gps_cnav(self, data: RawNav, **kwargs): def _format_rawnav_gal_fnav(self, data: RawNav, **kwargs): """ - Format RawNav GAL FNAV & INAV broadcast orbit blocks. + Format RawNav GALILEO FNAV & INAV broadcast orbit blocks. :param RawNav data: RawNav object containing data \ collated from UBX RXM-SFRBX messages or other \ @@ -1156,7 +1177,7 @@ def _format_rawnav_gal_fnav(self, data: RawNav, **kwargs): def _format_rawnav_bds_d1d2(self, data: RawNav, **kwargs): """ - Format RawNav BDS D1 & D2 (B1I, B2I, B3I) broadcast orbit blocks. + Format RawNav BEIDOU D1 & D2 (B1I, B2I, B3I) broadcast orbit blocks. :param RawNav data: RawNav object containing data \ collated from UBX RXM-SFRBX messages or other \ @@ -1227,7 +1248,7 @@ def _format_rawnav_bds_d1d2(self, data: RawNav, **kwargs): msgtype="D2" if d1d2 == 2 else "D1", msgsubtype="", timecode="BDUT", - utcid="UT(NTSC)", + utcid="UTC(NTSC)", data=data, ) if self._ionocorrflag: @@ -1240,7 +1261,7 @@ def _format_rawnav_bds_d1d2(self, data: RawNav, **kwargs): def _format_rawnav_glo_l1of(self, data: RawNav, **kwargs): """ - Format RawNav GLO L1OF (FDMA) broadcast orbit blocks. + Format RawNav GLONASS L1OF (FDMA) broadcast orbit blocks. :param RawNav data: RawNav object containing data \ collated from UBX RXM-SFRBX messages or other \ @@ -1294,8 +1315,6 @@ def _format_rawnav_glo_l1of(self, data: RawNav, **kwargs): if self._rinex_version < RINEX4: if self._timecorrflag: self._format_timecorr_3(data, 3) - # if self._ionocorrflag: - # self._format_ionocorr_3(data) else: # RINEX 4.02 if self._timecorrflag: nvd[STO] = self._format_timecorr_4( @@ -1305,10 +1324,369 @@ def _format_rawnav_glo_l1of(self, data: RawNav, **kwargs): utcid="UTC(SU)", data=data, ) - # if self._ionocorrflag: - # nvd[ION] = self._format_ionocorr_4( - # msgtype="IFNV", msgsubtype="", model=NEQUICK, data=data - # ) + + def _format_rawnav_sba_l1ca(self, data: RawNav, **kwargs): + """ + Format RawNav SBAS L1CA broadcast orbit blocks. + + :param RawNav data: RawNav object containing data \ + collated from UBX RXM-SFRBX messages or other \ + raw NAV subframe sources. + """ + + self._navdata[(data.svcode, data.toc)] = {} + nvd = self._navdata[(data.svcode, data.toc)] + + epoch, wn_cont = get_epoch(wno=data.wn, tow=data.tow, gnss=data.gnss) + self.__app.set_current_epoch(epoch, NAV) + + # derive health flag from MT17 record, if available + ura = getattr(data, "ura", 15) + health = 0 + for i in range(3): + prn = getattr(data, f"prn_{i+1:02d}", 0) + if prn == data.svid: + health = getattr(data, f"svhealth_{i+1:02d}") & 0b1111 + break + health |= int(data.subframeacq < 0b100) << 4 # MT17 health not present + health |= int(ura == 15) << 5 # do not use for ranging + + nvd[EPOCH] = epoch + nvd[RECTYPE] = "SBAS" + nvd[CLKBIAS] = data.agf0 # clock bias (sec) + nvd[CLKDRIFT] = data.agf1 # relative frequency bias (sec2) + nvd[CLKRATE] = data.t0 # time of message (sec) + nvd[BOD] = [] + nvb = nvd[BOD] + for _ in range(3): # broadcast orbit data blocks * 3 + nvb.append(["", "", "", ""]) # 4X,4D19.12 + # BROADCAST ORBIT - 1 + # NB raw position values are in meters + nvb[0][0] = data.xpos / 1000 # - Satellite position X (km) + nvb[0][1] = data.xdot / 1000 # - velocity X dot (km/sec) + nvb[0][2] = data.xdot2 / 1000 # - X acceleration (km/sec2) + nvb[0][3] = health # - health from MT17 + # BROADCAST ORBIT - 2 + nvb[1][0] = data.ypos / 1000 # - Satellite position Y (km) + nvb[1][1] = data.ydot / 1000 # - velocity Y dot (km/sec) + nvb[1][2] = data.ydot2 / 1000 # - Y acceleration (km/sec2) + nvb[1][3] = 32767 if ura == 15 else gpsura2m(ura) # SV accuracy in meters + # BROADCAST ORBIT - 3 + nvb[2][0] = data.zpos / 1000 # - Satellite position Z (km) + nvb[2][1] = data.zdot / 1000 # - velocity Z dot (km/sec) + nvb[2][2] = data.zdot2 / 1000 # - Z acceleration (km/sec2) + nvb[2][3] = data.iodn # - IODN (Issue of Data Navigation) + + if self._rinex_version < RINEX4: + if self._timecorrflag: + self._format_timecorr_3(data, 3) + else: # RINEX 4.02 + if self._timecorrflag: + nvd[STO] = self._format_timecorr_4( + msgtype="SBAS", + msgsubtype="", + timecode="SBUT", + utcid=UTCID.get(data.utcid, "N/A"), # EGNOS, WAAS, MSAS, etc. + data=data, + ) + + def _format_rawnav_qzs_lnav(self, data: RawNav, **kwargs): + """ + Format RawNav QZSS LNAV broadcast orbit blocks. + + :param RawNav data: RawNav object containing data \ + collated from UBX RXM-SFRBX messages or other \ + raw NAV subframe sources. + """ + + self._navdata[(data.svcode, data.iode)] = {} + nvd = self._navdata[(data.svcode, data.iode)] + + epoch, wn_cont = get_epoch(wno=data.wn, tow=data.tow, gnss=data.gnss) + self.__app.set_current_epoch(epoch, NAV) + nvd[EPOCH] = epoch + nvd[RECTYPE] = "LNAV" + nvd[CLKBIAS] = data.af0 # clock bias + nvd[CLKDRIFT] = data.af1 # clock drift + nvd[CLKRATE] = data.af2 # clock drift rate + nvd[BOD] = [] + nvb = nvd[BOD] + for _ in range(7): # broadcast orbit data blocks * 7 + nvb.append(["", "", "", ""]) # 4X,4D19.12 + # Multiply by pi to convert semicircles to radians + # BROADCAST ORBIT - 1 + nvb[0][0] = data.iode # - Issue of Data, Ephemeris + nvb[0][1] = data.crs # - Crs (meters) + nvb[0][2] = data.deltan * pi # - Delta n (radians/sec) + nvb[0][3] = data.m0 * pi # - M0 (radians) + # BROADCAST ORBIT - 2 + nvb[1][0] = data.cuc # - Cuc (radians) + nvb[1][1] = data.e # - e Eccentricity + nvb[1][2] = data.cus # - Cus (radians) + nvb[1][3] = data.sqrta # - sqrt(a) (sqrt(m)) + # BROADCAST ORBIT - 3 + nvb[2][0] = data.toe # - Toe Time of Ephemeris (sec of NAVIC week) + nvb[2][1] = data.cic # - Cic (radians) + nvb[2][2] = data.omega0 * pi # - OMEGA0 (radians) + nvb[2][3] = data.cis # - Cis (radians) + # BROADCAST ORBIT - 4 + nvb[3][0] = data.i0 * pi # - i0 (radians) + nvb[3][1] = data.crc # - Crc (meters) + nvb[3][2] = data.omega * pi # - omega (radians) + nvb[3][3] = data.omegadot * pi # - OMEGA DOT (radians/sec) + # BROADCAST ORBIT - 5 + nvb[4][0] = data.idot * pi # - IDOT (radians/sec) + nvb[4][1] = data.l2codes # codes on L2 channel + nvb[4][2] = wn_cont # - continuous week number, NOT mod 1024 + nvb[4][3] = data.l2pdata # - L2 P data + # BROADCAST ORBIT - 6 + nvb[5][0] = gpsura2m(data.ura) # SV accuracy in meters + nvb[5][1] = data.svhealth # - SV health (FLOAT → INTEGER) + nvb[5][2] = data.tgd # - TGD (seconds) + nvb[5][3] = data.iodc # Issue of Data, Clock + # BROADCAST ORBIT - 7 + nvb[6][0] = data.tow # HOW tow (17 LSB) + nvb[6][1] = get_fithours(data.iodc, data.fit, data.gnss) # FIT hours + nvb[6][2] = "" # - Spare + nvb[6][3] = "" # - Spare + + if self._rinex_version < RINEX4: + if self._timecorrflag: + self._format_timecorr_3(data, 2) + if self._ionocorrflag: + self._format_ionocorr_3(data) + else: # RINEX 4.02 + if self._timecorrflag: + nvd[STO] = self._format_timecorr_4( + msgtype="LNAV", + msgsubtype="", + timecode="GPUT", + utcid="UTC(USNO)", + data=data, + ) + if self._ionocorrflag: + nvd[ION] = self._format_ionocorr_4( + msgtype="LNAV", msgsubtype="", model=KLOB, data=data + ) + + def _format_rawnav_qzs_cnav(self, data: RawNav, **kwargs): + """ + Format RawNav QZSS CNAV broadcast orbit blocks. + + :param RawNav data: RawNav object containing data \ + collated from UBX RXM-SFRBX messages or other \ + raw NAV subframe sources. + """ + + self._navdata[(data.svcode, data.top)] = {} # have assumed top => iode + nvd = self._navdata[(data.svcode, data.top)] + + epoch, wn_cont = get_epoch(wno=data.wn, tow=data.tow, gnss=data.gnss) + self.__app.set_current_epoch(epoch, NAV) + nvd[EPOCH] = epoch + nvd[RECTYPE] = "CNAV" + nvd[CLKBIAS] = data.af0n # clock bias + nvd[CLKDRIFT] = data.af1n # clock drift + nvd[CLKRATE] = data.af2n # clock drift rate + nvd[BOD] = [] + nvb = nvd[BOD] + for _ in range(8): # broadcast orbit data blocks * 8 + nvb.append(["", "", "", ""]) # 4X,4D19.12 + # Multiply by pi to convert semicircles to radians + # BROADCAST ORBIT - 1 + nvb[0][0] = data.adot # - Issue of Data, Ephemeris SFR10 + nvb[0][1] = data.crs # - Crs (meters) SFR11 + nvb[0][2] = data.deltan0 * pi # - Delta n (radians/sec) SFR10 + nvb[0][3] = data.m0 * pi # - M0 (radians) SFR10 + # BROADCAST ORBIT - 2 + nvb[1][0] = data.cuc # - Cuc (radians) SFR11 + nvb[1][1] = data.e # - e Eccentricity SFR10 + nvb[1][2] = data.cus # - Cus (radians) SFR11 + # nvb[1][3] = data.sqrta # - sqrt(a) (sqrt(m)) SFR37 + nvb[1][3] = sqrt(AREF - data.deltaa) # - sqrt(a) (sqrt(m)) SFR10 + # BROADCAST ORBIT - 3 + nvb[2][0] = data.top # - Toe Time of Ephemeris (sec of NAVIC week) SFR10 + nvb[2][1] = data.cic # - Cic (radians) SFR11 + nvb[2][2] = data.omega0 * pi # - OMEGA0 (radians) SFR11 + nvb[2][3] = data.cis # - Cis (radians) SFR11 + # BROADCAST ORBIT - 4 + nvb[3][0] = data.i0 * pi # - i0 (radians) SFR11 + nvb[3][1] = data.crc # - Crc (meters) SFR11 + nvb[3][2] = data.omega * pi # - omega (radians) SFR10 + # nvb[3][3] = data.omegadot * pi # - OMEGA DOT (radians/sec) SFR37 + nvb[3][3] = ( + OMEGADOTREF - data.deltaomegadot + ) * pi # - OMEGA DOT (radians/sec) SFR11 + # BROADCAST ORBIT - 5 + nvb[4][0] = data.idot * pi # - IDOT (radians/sec) SFR11 + nvb[4][1] = data.deltan0 * pi # SFR10 + nvb[4][2] = data.uraned0 # - user range error NED0 SFRCLK + nvb[4][3] = data.uraned1 # - user range error NED1 SFRCLK + # BROADCAST ORBIT - 6 + nvb[5][0] = data.uraed # - SV Accuracy (metres) + nvb[5][1] = ( + data.l1health | (data.l2health << 1) | (data.l5health << 2) + ) # - L1,L2,L5 health SFR10 + nvb[5][2] = data.tgd # - TGD (seconds) SFR30 + nvb[5][3] = data.uraned2 # user range error NED2 SFRCLK + # BROADCAST ORBIT - 7 + nvb[6][0] = data.iscl1ca # iono delay SFR30 + nvb[6][1] = data.iscl2c # iono delay SFR30 + nvb[6][2] = data.iscl5i5 # iono delay SFR30 + nvb[6][3] = data.iscl5q5 # iono delay SFR30 + # BROADCAST ORBIT - 8 + nvb[7][0] = data.tow # time of transmission + nvb[7][1] = wn_cont # continuous week number SFR10 + nvb[7][2] = data.integrity | (data.l2phase << 1) | (data.alert << 2) + + if self._rinex_version < RINEX4: + if self._timecorrflag: + self._format_timecorr_3(data, 2) + if self._ionocorrflag: + self._format_ionocorr_3(data) + else: # RINEX 4.02 + if self._timecorrflag: + nvd[STO] = self._format_timecorr_4( + msgtype="CNVX", + msgsubtype="WIDE", + timecode="GPUT", + utcid="UTC(USNO)", + data=data, + ) + if self._ionocorrflag: + nvd[ION] = self._format_ionocorr_4( + msgtype="CNAV", msgsubtype="", model=KLOB, data=data + ) + if self._eopcorrflag: + try: + nvd[EOP] = format_eop( + svcode=data.svcode, + msgtype="CNVX", + msgsubtype="", + epoch=data.epoch, + tom=data.teop, + xp=data.pmx, + dxpdt=data.pmxdot, + dxpdt2=0, + yp=data.pmy, + dypdt=data.pmydot, + dypdt2=0, + deltaut1=data.deltaut1, + ddeltaut1dt=data.deltaut1dot, + d2deltaut1dt2=0, + ) + except AttributeError: + pass + + def _format_rawnav_qzs_cnv2(self, data: RawNav, **kwargs): + """ + Format RawNav QZSS CNV2 broadcast orbit blocks. + + :param RawNav data: RawNav object containing data \ + collated from UBX RXM-SFRBX messages or other \ + raw NAV subframe sources. + """ + + # TODO + + def _format_rawnav_irn_lnav(self, data: RawNav, **kwargs): + """ + Format RawNav IRNSS (NAVIC) LNAV broadcast orbit blocks. + + :param RawNav data: RawNav object containing data \ + collated from UBX RXM-SFRBX messages or other \ + raw NAV subframe sources. + """ + + self._navdata[(data.svcode, data.iodec)] = {} + nvd = self._navdata[(data.svcode, data.iodec)] + + epoch, wn_cont = get_epoch(wno=data.wn, tow=data.tow, gnss=data.gnss) + self.__app.set_current_epoch(epoch, NAV) + nvd[EPOCH] = epoch + nvd[RECTYPE] = "LNAV" + nvd[CLKBIAS] = data.af0 # clock bias + nvd[CLKDRIFT] = data.af1 # clock drift + nvd[CLKRATE] = data.af2 # clock drift rate + nvd[BOD] = [] + nvb = nvd[BOD] + for _ in range(7): # broadcast orbit data blocks * 7 + nvb.append(["", "", "", ""]) # 4X,4D19.12 + # Multiply by pi to convert semicircles to radians + # BROADCAST ORBIT - 1 + nvb[0][0] = data.iodec # - Issue of Data, Ephemeris + nvb[0][1] = data.crs # - Crs (meters) + nvb[0][2] = data.deltan * pi # - Delta n (radians/sec) + nvb[0][3] = data.m0 * pi # - M0 (radians) + # BROADCAST ORBIT - 2 + nvb[1][0] = data.cuc # - Cuc (radians) + nvb[1][1] = data.e # - e Eccentricity + nvb[1][2] = data.cus # - Cus (radians) + nvb[1][3] = data.sqrta # - sqrt(a) (sqrt(m)) + # BROADCAST ORBIT - 3 + nvb[2][0] = data.toe # - Toe Time of Ephemeris (sec of NAVIC week) + nvb[2][1] = data.cic # - Cic (radians) + nvb[2][2] = data.omega0 * pi # - OMEGA0 (radians) + nvb[2][3] = data.cis # - Cis (radians) + # BROADCAST ORBIT - 4 + nvb[3][0] = data.i0 * pi # - i0 (radians) + nvb[3][1] = data.crc # - Crc (meters) + nvb[3][2] = data.omega * pi # - omega (radians) + nvb[3][3] = data.omegadot * pi # - OMEGA DOT (radians/sec) + # BROADCAST ORBIT - 5 + nvb[4][0] = data.idot * pi # - IDOT (radians/sec) + nvb[4][1] = "" # blank + nvb[4][2] = wn_cont # - continuous week number, NOT mod 1024 + nvb[4][3] = "" # blank + # BROADCAST ORBIT - 6 + nvb[5][0] = gpsura2m(data.ura) # SV accuracy in meters + nvb[5][1] = data.l5flag << 1 & data.sflag # - SV health (FLOAT → INTEGER) + nvb[5][2] = data.tgd # - TGD (seconds) + nvb[5][3] = data.iodc # Issue of Data, Clock + # BROADCAST ORBIT - 7 + nvb[6][0] = data.towc # HOW tow (17 LSB) + nvb[6][1] = "" # - Spare + nvb[6][2] = "" # - Spare + nvb[6][3] = "" # - Spare + + if self._rinex_version < RINEX4: + if self._timecorrflag: + self._format_timecorr_3(data, 2) + if self._ionocorrflag: + self._format_ionocorr_3(data) + else: # RINEX 4.02 + if self._timecorrflag: + nvd[STO] = self._format_timecorr_4( + msgtype="LNAV", + msgsubtype="", + timecode="IRUT", + utcid="UTCIRN", + data=data, + ) + if self._ionocorrflag: + nvd[ION] = self._format_ionocorr_4( + msgtype="LNAV", msgsubtype="", model=KLOB, data=data + ) + if self._eopcorrflag: + try: + nvd[EOP] = format_eop( + svcode=data.svcode, + msgtype="LNAV", + msgsubtype="", + epoch=data.epoch, + tom=data.teop, + xp=data.pmx, + dxpdt=data.pmxdot, + dxpdt2=0, + yp=data.pmy, + dypdt=data.pmydot, + dypdt2=0, + deltaut1=data.deltaut1, + ddeltaut1dt=data.deltaut1dot, + d2deltaut1dt2=0, + ) + except AttributeError: + pass def _format_timecorr_3(self, data: RawNav, source: int = 0): """ @@ -1330,10 +1708,14 @@ def _format_timecorr_3(self, data: RawNav, source: int = 0): a1 = data.a1 timeref = data.toc weekno = data.wn + if data.gnss == SBA: + svcode = SBASPRN.get(data.svid, "N/A") + else: + svcode = data.svcode timecode = f"{RINEXGNSSR[data.gnss][0:2]}UT" self._timecorr[timecode] = format_time_corr( corrtype=timecode, - svcode=data.svcode, + svcode=svcode, source=source, timeref=timeref, weekno=weekno, @@ -1376,7 +1758,7 @@ def _format_timecorr_4( msgsubtype=msgsubtype, epoch=epoch, timecode=timecode, - sbasid="", + sbasid=SBASPRN.get(data.svid, "N/A") if data.gnss == SBA else "", utcid=utcid, tot=tot, a0=a0, diff --git a/src/pygnssutils/rinex_conv_obs.py b/src/pygnssutils/rinex_conv_obs.py index f59ae6e..b3bc27c 100644 --- a/src/pygnssutils/rinex_conv_obs.py +++ b/src/pygnssutils/rinex_conv_obs.py @@ -3,15 +3,11 @@ RINEX Conversion Observation class. -A preliminary implementation of a RINEX observation conversion utility. - Converts UBX RXM-RAWX messages to RINEX Observation text format. Observation data comprises pseudorange, (carrier) phaserange, Doppler shift and signal strength. -Functionality will be extended in future versions - contributions welcome. - Created on 6 Oct 2025 :author: semuadmin (Steve Smith) @@ -71,7 +67,7 @@ format_timefirstlast, get_epoch, get_obscode_ubx, - get_svcode_ubx, + get_svcode, ) @@ -458,7 +454,7 @@ def geta(att: str, i: int): cp = geta("cpMes", i) do = geta("doMes", i) cno = geta("cno", i) - svcode = get_svcode_ubx(gnss, svid) + svcode = get_svcode(gnssr, svid) obscode = get_obscode_ubx(gnss, sigid) # ignore any filtered out gnss diff --git a/src/pygnssutils/rinex_globals.py b/src/pygnssutils/rinex_globals.py index add9f95..4bd9089 100644 --- a/src/pygnssutils/rinex_globals.py +++ b/src/pygnssutils/rinex_globals.py @@ -43,6 +43,7 @@ ION = "ION" IRN = "I" KLOB = "KLOBUCHAR" +L1CA = "L1CA" L1OF = "L1OF" L2OF = "L2OF" LEAPS0 = datetime(1900, 1, 1, 0, 0, 0, tzinfo=timezone.utc) @@ -99,6 +100,7 @@ P2_N25 = 2.9802322387695312e-08 # 2**-25 P2_N26 = 1.4901161193847656e-08 # 2**-26 P2_N27 = 7.450580596923828e-09 # 2**-27 +P2_N28 = 3.725290298461914e-09 # 2**-28 P2_N29 = 1.862645149230957e-09 # 2**-29 P2_N30 = 9.313225746154785e-10 # 2**-30 P2_N31 = 4.656612873077393e-10 # 2**-31 @@ -109,6 +111,7 @@ P2_N37 = 7.275957614183426e-12 # 2**-37 P2_N38 = 3.637978807091713e-12 # 2**-38 P2_N40 = 9.094947017729282e-13 # 2**-40 +P2_N41 = 4.547473508864641e-13 # 2**-41 P2_N43 = 1.1368683772161603e-13 # 2**-43 P2_N44 = 5.684341886080802e-14 # 2**-44 P2_N46 = 1.4210854715202004e-14 # 2**-46 @@ -144,7 +147,7 @@ """RINEX GNSS Codes.""" RINEXOBSPREFIX = { - BDS: "Pseudo Range", + "C": "Pseudo Range", "L": "Carrier Phase", "D": "Doppler Shift", "S": "Signal Strength", @@ -201,7 +204,6 @@ 1: SBA, # SBAS 2: GAL, # GAL 3: BDS, # BDS - # 4: "N", # N/A 5: QZS, # QZSS 6: GLO, # GLO 7: IRN, # NavIC @@ -215,38 +217,38 @@ UBXRINEXOBSCODE = { (0, 0): "1C", # GPS L1 C/A Legacy LNAV - (0, 1): "1S", # GPS L1C D Data code CNV2 (not yet implemented by u-blox) - (0, 2): "1L", # GPS L1C P Pilot code CNV2 (not yet implemented by u-blox) + # (0, 1): "1S", # GPS L1C D Data code CNV2 (not yet implemented by u-blox) * + # (0, 2): "1L", # GPS L1C P Pilot code CNV2 (not yet implemented by u-blox) * (0, 3): "2L", # GPS L2C L Civil Long-length code CNAV (0, 4): "2S", # GPS L2C M Civil Moderate code CNAV (0, 6): "5I", # GPS L5 I In-phase code CNAV - (0, 7): "5Q", # GPS L5 Q Quadrature code CNAV + (0, 7): "5Q", # GPS L5 Q Quadrature code CNAV * (1, 0): "1C", # SBA L1 C/A - (2, 0): "1C", # GAL E1_C - (2, 1): "1B", # GAL E1_B INAV - (2, 3): "5I", # GAL E5_aI FNAV - (2, 4): "5Q", # GAL E5_aQ - (2, 5): "7I", # GAL E5_bI INAV - (2, 6): "7Q", # GAL E5_bQ - (2, 8): "6B", # GAL E6_B - (2, 9): "6C", # GAL E6_C + (2, 0): "1C", # GAL E1_C * + (2, 1): "1B", # GAL E1 (E1_B) INAV + (2, 3): "5I", # GAL E5a (E5_aI) FNAV + (2, 4): "5Q", # GAL E5_aQ * + (2, 5): "7I", # GAL E5_bI INAV * + (2, 6): "7Q", # GAL E5_bQ * + (2, 8): "6B", # GAL E6 (E6_B) CNAV + (2, 9): "6C", # GAL E6_C * (3, 0): "2I", # BDS B1I_D1 D1 (3, 1): "2I", # BDS B1I_D2 D2 - (3, 2): "7I", # BDS B2I_D1 D1 - (3, 3): "7I", # BDS B2I_D2 D2 + (3, 2): "7I", # BDS B2I_D1 D1 * + (3, 3): "7I", # BDS B2I_D2 D2 * (3, 4): "6I", # BDS B3I_D1 D1 - (3, 5): "1P", # BDS B1_Cp - (3, 6): "1D", # BDS B1_Cd CNV1 - (3, 7): "5P", # BDS B2_ap - (3, 8): "5D", # BDS B2_ad CNV2 (3, 10): "6I", # BDS B3I_D2 D2 - (5, 0): "1C", # QZS L1_C/A + (3, 5): "1P", # BDS B1_Cp * + (3, 6): "1D", # BDS B1C (B1_Cd) CNV1 + (3, 7): "5P", # BDS B2_ap * + (3, 8): "5D", # BDS B2A (B2_ad) CNV2 * + (5, 0): "1C", # QZS L1_C/A LNAV (5, 1): "1Z", # QZS L1_S - (5, 4): "2S", # QZS L2_CM + (5, 4): "2S", # QZS L2_CM CNAV (5, 5): "2L", # QZS L2_CL - (5, 8): "5I", # QZS L5_I - (5, 9): "5Q", # QZS L5_Q - (5, 12): "1B", # QZS L1_CB + (5, 8): "5I", # QZS L5_I CNAV + (5, 9): "5Q", # QZS L5_Q * + (5, 12): "1B", # QZS L1_CB CNV2 * (6, 0): "1C", # GLO L1_OF (6, 2): "2C", # GLO L2_OF (7, 0): "5A", # IRN L5_A @@ -254,8 +256,59 @@ """ UBX Signal ID -> RINEX Observation Code Lookup. TODO CHECK THIS MAPPING!!! -- key is (msg.gnss, msg.sigId) where msg = UBX-RXM-RAWX +- key is (msg.gnss, msg.sigId) where msg = UBX-RXM-RAWX or UBX-RXM-SFRBX - value is RINEX Observation Code, minus prefix (see RINEXOBSPREFIX for appropriate prefix) + +* - not implemented on ZED-X20 series +""" + +UTCID = { + 0: "UTC(CRL)", + 1: "UTC(NIST)", + 2: "UTC(USNO)", + 3: "UTC(BIPM)", + 4: "UTC(GAL)", + 7: "N/A", +} +"""SBAS UTC Time Source Identifier.""" + +SBASPRN = { + 120: "EGNOS", # Europe + 121: "EGNOS", + 122: "SPAN", # SouthPAN - Australia/New Zealand + 123: "EGNOS", + 124: "SPAN", + 125: "SDCM", # Russia + 126: "EGNOS", + 127: "GAGAN", # India + 128: "GAGAN", + 129: "MSAS", # Japan + 130: "BDSBAS", # China + 131: "WAAS", # USA + 132: "GAGAN", + 133: "WAAS", + 134: "KAAS", # Korea + 135: "WAAS", + 136: "EGNOS", + 137: "MSAS", + 138: "WAAS", + 140: "SDCM", + 141: "SDCM", + 143: "BDSBAS", + 144: "BDSBAS", + 145: "BDSBAS", + 146: "BDSBAS", + 147: "NSAS", # Africa + 148: "ASAL", # Algeria + 158: "WAAS", +} +""" +SBAS PRN allocation. + +NB: This is a dynamic mapping as PRNs transition +from allocated -> test -> operational -> decommissioned. + +https://media.defense.gov/2019/Aug/01/2002165758/-1/-1/1/L1%20C-A%2026%20APR%2019.PDF """ RINEX_PHASE_ALIGNMENT = { @@ -399,7 +452,7 @@ (GPS, "L2C"): "CNAV", # L2C (GPS, "L5"): "CNAV", # L5 (IRN, "L1"): "L1NV", # L1 - (IRN, "L5/S SPS"): "LNAV", # L5/S SPS + (IRN, "L5/S SPS"): "LNAV", # IRN L5_A (QZS, "L1 C/A"): "LNAV", # L1 C/A (QZS, "L1 C/B"): "LNAV", # L1 C/B (QZS, "L1C"): "CNV2", # L1C diff --git a/src/pygnssutils/rinex_helpers.py b/src/pygnssutils/rinex_helpers.py index 01f9139..a95579e 100644 --- a/src/pygnssutils/rinex_helpers.py +++ b/src/pygnssutils/rinex_helpers.py @@ -43,7 +43,6 @@ TIME_BEIDOU, TIME_GPS, TIME_UNDEFINED, - UBXRINEXGNSS, UBXRINEXOBSCODE, ) @@ -212,25 +211,28 @@ def adjust_time_units(value: float) -> tuple[int, str]: return 0, "U" # undefined -def get_svcode_ubx(gnssid: int, svid: int, leadzero: bool = True) -> str: +def get_svcode(gnssr: str, svid: int, leadzero: bool = True) -> str: """ - Convert UBX gnssid and svid values to RINEX svcode e.g "G29", "E 4". + Convert gnss code and svid value to RINEX svcode e.g "G29", "E 4". SBAS and QZSS SV ID ranges are adjusted to range 0 - 32. - :param int gnssid: UBX GNSS id e.g. 0, 1 + :param str gnssr: GNSS code e.g. "G" :param int svid: UBX SV id e.g. 14 :param bool leadzero: leading zeros :return: svcode as string :rtype: str """ - gnssr = UBXRINEXGNSS[gnssid] - if gnssr == SBA: # SBAS - svid -= 100 - elif gnssr == QZS: # QZSS - svid -= 192 - return get_svcode(gnssr=gnssr, svid=svid, leadzero=leadzero) + if gnssr == SBA and svid > 100: # SBAS + svida = svid - 100 + elif gnssr == QZS and svid > 192: # QZSS + svida = svid - 192 + else: + svida = svid + if leadzero: + return f"{gnssr}{svida:02d}" + return f"{gnssr}{svida:>2}" def get_obscode_ubx(gnss: int, sigid: int) -> str: @@ -241,25 +243,6 @@ def get_obscode_ubx(gnss: int, sigid: int) -> str: return UBXRINEXOBSCODE[(gnss, sigid)] -def get_svcode( - gnssr: str, svid: int, freqno: int | NoneType = None, leadzero: bool = True -) -> str: - """ - Convert RINEX gnss and svid values to svcode e.g "G29", "E 4" - - :param str gnss: RINEX GNSS code e.g. "G", "R" - :param int svid: sv id e.g. 14 - :param int | NoneType freqno: GLONASS frequency no - :param bool leadzero: leading zeros - :return: svcode as string - :rtype: str - """ - - if leadzero: - return f"{gnssr}{svid:02d}" - return f"{gnssr}{svid:>2}" - - def get_ssi(cno: float) -> int: """ Convert CNO in dbHz to RINEX signal strength indicator (SSI) value @@ -1139,7 +1122,8 @@ def format_eop( f"{epoch.minute:02d} {epoch.second:02d}{DRNX(xp, 19,12)}{DRNX(dxpdt, 19,12)}" f"{DRNX(dxpdt2, 19,12)}\n" f" {'':<19}{DRNX(yp, 19,12)}{DRNX(dypdt, 19,12)}{DRNX(dypdt2, 19,12)}\n" - f" {DRNX(tom,19,12)}{DRNX(deltaut1, 19,12)}{DRNX(ddeltaut1dt, 19,12)}{DRNX(d2deltaut1dt2, 19,12)}\n" + f" {DRNX(tom,19,12)}{DRNX(deltaut1, 19,12)}" + f"{DRNX(ddeltaut1dt, 19,12)}{DRNX(d2deltaut1dt2, 19,12)}\n" ) return out @@ -1149,7 +1133,7 @@ def format_time_corr( corrtype: str, timeref: int, weekno: int, - source: str, + source: int, a0: float, a1: float, ) -> str: @@ -1160,7 +1144,7 @@ def format_time_corr( :param str corrtype: correction type :param int timeref: time reference :param int weekno: week number - :param str source: time source + :param int source: time source :param float a0: a0 clock offset :param float a1: a1 clock offset :return: formatted string diff --git a/tests/test_rawnav.py b/tests/test_rawnav.py index ac7eff3..c25c672 100644 --- a/tests/test_rawnav.py +++ b/tests/test_rawnav.py @@ -13,7 +13,7 @@ from datetime import datetime, timezone from pygnssutils.exceptions import RINEXProcessingError from pygnssutils.rawnav import RawNav, PREAMBLE, VALPREAMBLE, U, S -from pygnssutils.rinex_subframes_gps import ( +from pygnssutils.rawnav_subframes_gps import ( GPS_LNAV_SUBFRAME_1, GPS_LNAV_SUBFRAME_3, GPS_LNAV_SUBFRAME_2, diff --git a/tests/test_rinex.py b/tests/test_rinex.py index 33afd66..adf948f 100644 --- a/tests/test_rinex.py +++ b/tests/test_rinex.py @@ -15,6 +15,17 @@ from pathlib import Path from time import sleep +from pygnssutils.rawnav import RawNav +from pygnssutils.rawnav_subframes_glo import ( + GLO_L1OF_SUBFRAME_1, + GLO_L1OF_SUBFRAME_2, + GLO_L1OF_SUBFRAME_3, +) +from pygnssutils.rawnav_subframes_bds import ( + BDS_D1_SUBFRAME_1, + BDS_D1_SUBFRAME_2, + BDS_D1_SUBFRAME_3, +) from pygnssutils.rinex_conv import RinexConverter from pygnssutils.rinex_globals import BDS, EPOCH0_GPS, EPOCHMAX, EPOCHMIN, GAL, GPS, IRN from pygnssutils.rinex_helpers import ( @@ -64,7 +75,6 @@ get_epoch, get_epoch_glo, get_svcode, - get_svcode_ubx, gpsura2m, glotk2sec, listify, @@ -166,23 +176,21 @@ def testDRNX(self): self.assertEqual(DRNX("", 14, 7), " ") self.assertEqual(DRNX(" ", 14, 8), " ") - def testgetsvcodeubx(self): - self.assertEqual(get_svcode_ubx(0, 3), "G03") - self.assertEqual(get_svcode_ubx(0, 3, False), "G 3") - self.assertEqual(get_svcode_ubx(2, 12), "E12") - self.assertEqual(get_svcode_ubx(2, 12, False), "E12") - self.assertEqual(get_svcode_ubx(1, 112), "S12") - self.assertEqual(get_svcode_ubx(1, 112, False), "S12") - self.assertEqual(get_svcode_ubx(5, 194), "J02") - self.assertEqual(get_svcode_ubx(5, 194, False), "J 2") - - def testgetsvcodertcm(self): + def testgetsvcode(self): + self.assertEqual(get_svcode("G", 3), "G03") + self.assertEqual(get_svcode("G", 3, False), "G 3") + self.assertEqual(get_svcode("E", 12), "E12") + self.assertEqual(get_svcode("E", 12, False), "E12") + self.assertEqual(get_svcode("S", 112), "S12") + self.assertEqual(get_svcode("S", 112, False), "S12") + self.assertEqual(get_svcode("J", 194), "J02") + self.assertEqual(get_svcode("J", 194, False), "J 2") self.assertEqual(get_svcode("G", 3), "G03") - self.assertEqual(get_svcode("G", 3, None, False), "G 3") + self.assertEqual(get_svcode("G", 3, False), "G 3") self.assertEqual(get_svcode("S", 112 - 100), "S12") - self.assertEqual(get_svcode("S", 112 - 100, None, False), "S12") + self.assertEqual(get_svcode("S", 112 - 100, False), "S12") self.assertEqual(get_svcode("J", 194 - 192), "J02") - self.assertEqual(get_svcode("J", 194 - 192, None, False), "J 2") + self.assertEqual(get_svcode("J", 194 - 192, False), "J 2") def testformat_filename(self): firstobs = datetime(2026, 3, 14, 12, 4, 6) @@ -544,87 +552,3 @@ def testgpsura2m(self): self.assertEqual(gpsura2m(15), 0) self.assertEqual(gpsura2m(-16), 0) self.assertEqual(gpsura2m(-8), 0.1) - - def testrinexnav(self): - EXPECTED_RESULT_OBS = [ - r" 3.05 O: OBSERVATION M: MIXED RINEX VERSION / TYPE\n", - r"PYRINEXCONV 0.1.0 ALSTEVE \b\d{8}\b \b\d{6}\b UTC PGM / RUN BY / DATE\n", - r"RinexConverter 0.1.0 NAV test COMMENT\n", - r"LOCAL MARKER NAME\n", - r"1 MARKER NUMBER\n", - r"GEODETIC MARKER TYPE\n", - r"semuadmin OBSERVER / AGENCY\n", - r"1 ublox X20P HPG 2.02 REC # / TYPE / VERS\n", - r"1 Beitian BT-184 ANT # / TYPE\n", - r" 3803648.1838 -148798.4259 5100640.5407 APPROX POSITION XYZ\n", - r" 0.0000 0.0000 0.0000 ANTENNA: DELTA H/E/N\n", - r"G 12 C5Q L5Q D5Q S5Q C1C L1C D1C S1C C2L L2L D2L S2L SYS / # / OBS TYPES\n", - r"C 12 C5P L5P D5P S5P C6C L6C D6C S6C C1P L1P D1P S1P SYS / # / OBS TYPES\n", - r"E 12 C5Q L5Q D5Q S5Q C1C L1C D1C S1C C6B L6B D6B S6B SYS / # / OBS TYPES\n", - r"DBHZ SIGNAL STRENGTH UNIT\n", - r" 1.000 INTERVAL\n", - r" 2026 4 24 8 47 32.0040000 GPS TIME OF FIRST OBS\n", - r" 2026 4 24 9 7 19.0030000 GPS TIME OF LAST OBS\n", - r" GLONASS SLOT / FRQ\n", - r" C1C 0.000 C1P 0.000 C2C 0.000 C2P 0.000 GLONASS COD/PHS/BIS\n", - r" 18 2415 5GPS LEAPSECONDS\n", - r" 34 # OF SATELLITES\n", - r" END OF HEADER\n", - r"> 2026 4 24 8 47 32.0040000 0 27 \n", - r"G26 21437036.766 84123533.8310 209.268 48.000 21437038.61\n", - r"→4 112652397.0350 280.161 53.000 21437038.650 87781089.8\n", - r"→610 218.296 44.000 \n", - r"G27 24519701.570 96220582.5730 2916.127 42.000 24519698.27\n", - r"→4 128851890.4480 3904.912 40.000 24519702.984 100404084.7\n", - r"→711 3041.609 24.000 \n", - ] - - EXPECTED_RESULT_NAV = [ - r" 3.05 N: NAVIGATION M: MIXED RINEX VERSION / TYPE\n", - r"PYRINEXCONV 0.1.0 ALSTEVE \b\d{8}\b \b\d{6}\b UTC PGM / RUN BY / DATE\n", - r"RinexConverter 0.1.0 NAV test COMMENT\n", - r"GPSA 1.8626e-08 1.4901e-08 -1.1921e-07 -1.1921e-07 V 29 IONOSPHERIC CORR\n", - r"GPSB 1.1469e\+05 6.5536e\+04 -1.9661e\+05 -6.5536e\+04 V 29 IONOSPHERIC CORR\n", - r"GPUT 0.0000000000e\+00-4.656612873e-09 61440 112 G56 0 TIME SYSTEM CORR\n", - r" 18 2415 5GPS LEAPSECONDS\n", - r" END OF HEADER\n", - r"G26 2026 04 24 09 59 42-3.574695438147e-04-4.433786671143e-12 0.000000000000e\+00\n", - r" 9.300000000000e\+01 2.503125000000e\+01 1.804437488317e-09 5.920644043945e-01\n", - r" 1.300126314163e-06 1.103244593833e-02 7.566064596176e-06 5.153741914749e\+03\n", - r" 4.680000000000e\+05-3.576278686523e-07-3.539365250617e-03 4.284083843231e-08\n", - r" 2.955303755589e-01 2.191875000000e\+02 2.267913562246e-01-2.714386937441e-09\n", - r" 5.184119800106e-11 3.670000000000e\+02 \n", - r" 0.000000000000e\+00 0.000000000000e\+00 6.519258022308e-09 \n", - r" 7.747300000000e\+04 \n", - ] - - if RINEXFILETEST is False: - return - rc = RinexConverter( - app="cliapp", - rinex_version="3.05", - rinex_types=[""], - gnssfilter=[""], - obsfilter=[""], - datasource=["R", "R", "R"], - minobs=10, - marker=["LOCAL", "1", "GEODETIC"], - antenna=["1", "Beitian BT-184"], - receiver=["1", "ublox X20P", "HPG 2.02"], - observer="semuadmin", - comments=["RinexConverter 0.1.0 NAV test"], - ) - rc.process_input("tests/pygpsdata_x20p_rxmsfrbx.log") - sleep(0.1) - with open("tests/pygpsdata_R_202604240959_16S_16S_MN.rnx", "r") as infile: - for i, ln in enumerate(infile.readlines()): - # print(ln) - self.assertRegex(ln, EXPECTED_RESULT_NAV[i]) - if i == 15: - break - with open("tests/pygpsdata_R_202604240847_20M_01S_MO.rnx", "r") as infile: - for i, ln in enumerate(infile.readlines()): - # print(ln) - self.assertRegex(ln, EXPECTED_RESULT_OBS[i]) - if i == 29: - break diff --git a/tests/test_rinex_defs.py b/tests/test_rinex_defs.py index cef95da..b85d67b 100644 --- a/tests/test_rinex_defs.py +++ b/tests/test_rinex_defs.py @@ -11,10 +11,13 @@ import unittest -import pygnssutils.rinex_subframes_bds as bds -import pygnssutils.rinex_subframes_gal as gal -import pygnssutils.rinex_subframes_glo as glo -import pygnssutils.rinex_subframes_gps as gps +import pygnssutils.rawnav_subframes_bds as bds +import pygnssutils.rawnav_subframes_gal as gal +import pygnssutils.rawnav_subframes_glo as glo +import pygnssutils.rawnav_subframes_gps as gps +import pygnssutils.rawnav_subframes_sba as sba +import pygnssutils.rawnav_subframes_qzs as qzs +import pygnssutils.rawnav_subframes_irn as irn from pygnssutils.rawnav import VALPREAMBLE @@ -39,7 +42,7 @@ def scandefs(self, sfrdefs, sfrlen): totlen += len self.assertEqual(totlen, sfrlen) - def testGPSLNAVdefs(self): + def testGPSLNAV(self): sfrdefs = ( gps.GPS_LNAV_SUBFRAME_1, @@ -50,7 +53,7 @@ def testGPSLNAVdefs(self): sfrlen = 300 self.scandefs(sfrdefs, sfrlen) - def testGPSCNAVdefs(self): + def testGPSCNAV(self): sfrdefs = ( gps.GPS_CNAV_SUBFRAME_10, gps.GPS_CNAV_SUBFRAME_11, @@ -146,3 +149,64 @@ def testGLOL1OF(self): ) sfrlen = 85 self.scandefs(sfrdefs, sfrlen) + + def testSBAL1CA(self): + + sfrdefs = ( + sba.SBA_L1CA_MT_9, + sba.SBA_L1CA_MT_12, + sba.SBA_L1CA_MT_17, + ) + sfrlen = 250 + self.scandefs(sfrdefs, sfrlen) + + def testQZSLNAV(self): + + sfrdefs = ( + qzs.QZS_LNAV_SUBFRAME_1, + qzs.QZS_LNAV_SUBFRAME_2, + qzs.QZS_LNAV_SUBFRAME_3, + qzs.QZS_LNAV_SUBFRAME_4_P56, + qzs.QZS_LNAV_SUBFRAME_5_P56, + ) + sfrlen = 300 + self.scandefs(sfrdefs, sfrlen) + + def testQZSCNAV(self): + sfrdefs = ( + qzs.QZS_CNAV_SUBFRAME_10, + qzs.QZS_CNAV_SUBFRAME_11, + qzs.QZS_CNAV_SUBFRAME_30, + qzs.QZS_CNAV_SUBFRAME_32, + qzs.QZS_CNAV_SUBFRAME_33, + ) + sfrlen = 300 + self.scandefs(sfrdefs, sfrlen) + + def testQZSCNV2_2(self): + sfrdefs = ( + qzs.QZS_CNV2_SUBFRAME_2, + ) + sfrlen = 600 + self.scandefs(sfrdefs, sfrlen) + + def testQZSCNV2_3(self): + sfrdefs = ( + qzs.QZS_CNV2_SUBFRAME_3_P1, + qzs.QZS_CNV2_SUBFRAME_3_P2, + qzs.QZS_CNV2_SUBFRAME_3_P61, + ) + sfrlen = 274 + self.scandefs(sfrdefs, sfrlen) + + def testIRNLNAV(self): + + sfrdefs = ( + irn.IRN_LNAV_SUBFRAME_1, + irn.IRN_LNAV_SUBFRAME_2, + # irn.IRN_LNAV_SUBFRAME_3_P5, + irn.IRN_LNAV_SUBFRAME_3_P9, + irn.IRN_LNAV_SUBFRAME_3_P11, + ) + sfrlen = 292 + self.scandefs(sfrdefs, sfrlen) diff --git a/tests/test_rinex_parse.py b/tests/test_rinex_parse.py new file mode 100644 index 0000000..e49b505 --- /dev/null +++ b/tests/test_rinex_parse.py @@ -0,0 +1,491 @@ +""" +Parse tests for pygnssutils rinex conversion + +Created on 26 May 2022 + +*** NB: must be saved in UTF-8 format *** + +@author: semuadmin +""" + +# pylint: disable=line-too-long + +import unittest + +from pygnssutils.rawnav import RawNav +from pygnssutils.rawnav_subframes_glo import ( + GLO_L1OF_SUBFRAME_1, + GLO_L1OF_SUBFRAME_2, + GLO_L1OF_SUBFRAME_3, +) +from pygnssutils.rawnav_subframes_bds import ( + BDS_D1_SUBFRAME_1, + BDS_D1_SUBFRAME_2, + BDS_D1_SUBFRAME_3, +) +from pygnssutils.rawnav_subframes_sba import ( + SBA_L1CA_MT_9, + SBA_L1CA_MT_12, + SBA_L1CA_MT_17, +) +from pygnssutils.rinex_helpers import adjust_time_units,gpsura2m,listify + +# only run RINEX file tests locally +RINEXFILETEST = False # system() == "Darwin" + + +class StaticTest(unittest.TestCase): + def setUp(self): + self.maxDiff = None + + def tearDown(self): + pass + + def testadjust_time_units(self): + self.assertEqual(adjust_time_units(34), (34, "S")) + self.assertEqual(adjust_time_units(345), (6.0, "M")) + self.assertEqual(adjust_time_units(4800), (80, "M")) + self.assertEqual(adjust_time_units(12345), (3.0, "H")) + self.assertEqual(adjust_time_units(1958400), (23.0, "D")) + self.assertEqual(adjust_time_units(1958400000), (62.0, "Y")) + self.assertEqual(adjust_time_units("asdfa"), (0, "U")) + self.assertEqual(adjust_time_units(1.23e20), (0, "U")) + + def testlistify(self): + self.assertEqual(listify("first,second,third"), ["first", "second", "third"]) + self.assertEqual(listify("first, second, third "), ["first", "second", "third"]) + self.assertEqual( + listify(["first", "second", "third"]), ["first", "second", "third"] + ) + self.assertEqual(listify("test"), ["test"]) + self.assertEqual(listify(""), [""]) + self.assertEqual(listify(None), [""]) + self.assertEqual(listify([""]), [""]) + + def testgpsura2m(self): + + self.assertEqual(gpsura2m(1), 2.8) + self.assertEqual(gpsura2m(2), 4.0) + self.assertEqual(gpsura2m(3), 5.7) + self.assertEqual(gpsura2m(5), 11.3) + self.assertEqual(gpsura2m(8), 64) + self.assertEqual(gpsura2m(14), 4096) + self.assertEqual(gpsura2m(15), 0) + self.assertEqual(gpsura2m(-16), 0) + self.assertEqual(gpsura2m(-8), 0.1) + + def testrawnavparserGLO( + self, + ): # test parser produces correct scaled values from subframe bits + EXPECTED_RESULT = "" + BITS = ( + ( + ( + "0", # idle + "0001", # sid + "00", # reserved + "01", # p1 + "000110001110", # tk + "000000000000000000000111", # xntbdot + "11001", # xntbdot2 + "000000000000000000000001111", # xntb + "00000000", # hamming + ), + GLO_L1OF_SUBFRAME_1, + ), + ( + ( + "0", # idle + "0010", # sid + "001", # bn + "1", # p2 + "0000111", # tb + "00000", # reserved + "000000000000000000001111", # yntbdot + "10001", # yntbdot2 + "000000000000000000000011111", # yntb + "00000000", # hamming + ), + GLO_L1OF_SUBFRAME_2, + ), + ( + ( + "0", # idle + "0011", # sid + "1", # p3 + "00000000111", # gammantb + "0", # reserved + "01", # p + "1", # ln + "000000000000000000011111", # zntbdot + "11101", # zntbdot2 + "000000000000000000000111111", # zntb + "00000000", # hamming + ), + GLO_L1OF_SUBFRAME_3, + ), + ) + raw = RawNav("R", 2, "1C") + for data, dic in BITS: + sfrbits = "0b" + "".join(data) + self.assertEqual(len(sfrbits), 85 + 2) + raw.parse(int(sfrbits, 2), dic, 0b111) + self.assertEqual(raw.gnss, "R") + self.assertEqual(raw.svid, 2) + self.assertEqual(raw.sigcode, "1C") + self.assertEqual(raw.svcode, "R 2") + self.assertEqual(raw.identity, "R021C") + self.assertEqual(raw.sid, 3) + self.assertEqual(raw.subframeacq, 7) + self.assertEqual(raw.tk, 398) + self.assertEqual(raw.tb, 7 * 15) + self.assertEqual(raw.gammantb, 7 * 2**-40) + self.assertEqual(raw.xntbdot, 7 * 2**-20) + self.assertEqual(raw.xntbdot2, -7 * 2**-30) + self.assertEqual(raw.xntb, 15 * 2**-11) + self.assertEqual(raw.yntbdot, 15 * 2**-20) + self.assertEqual(raw.yntbdot2, -15 * 2**-30) + self.assertEqual(raw.yntb, 31 * 2**-11) + self.assertEqual(raw.zntbdot, 31 * 2**-20) + self.assertEqual(raw.zntbdot2, -3 * 2**-30) + self.assertEqual(raw.zntb, 63 * 2**-11) + self.assertEqual(str(raw), EXPECTED_RESULT) + + def testrawnavparserBDS( + self, + ): # test parser produces correct scaled values from subframe bits + EXPECTED_RESULT = "" + BITS = ( + ( + ( + "11100010010", # preamble + "0000", # rev + "001", # sid + "00000111", # tow_msb + "0000", # _parity1 + "000000000111", # tow_lsb + "0", # sath1 + "00000", # aodc + "0000", # urai + "00000000", # _parity2 + "0000000000000", # wn + "000000000", # toc_msb + "00000000", # _parity3 + "00000000", # toc_lsb + "0000000000", # tgd1 + "0000", # tgd2_msb + "00000000", # _parity4 + "000000", # tgd2_lsb + "00000000", # alpha0 + "00000000", # alpha1 + "00000000", # _parity5 + "00000000", # alpha2 + "00000000", # alpha3 + "000000", # beta0_msb + "00000000", # _parity6 + "00", # beta0_lsb + "00000000", # beta1 + "00000000", # beta2 + "0000", # beta3_msb + "00000000", # _parity7 + "0000", # beta3_lsb + "00000000000", # af2 + "0000000", # af0_msb + "00000000", # _parity8 + "00000000000000000", # af0_lsb + "00000", # af1_msb + "00000000", # _parity9 + "00000000000000000", # af1_lsb + "00000", # aode + "00000000", # _parity10 + ), + BDS_D1_SUBFRAME_1, + ), + ( + ( + "11100010010", # preamble + "0000", # rev + "010", # sid + "00000111", # tow_msb + "0000", # _parity1 + "000000000111", # tow_lsb + "0000000000", # deltan_msb + "00000000", # _parity2 + "000000", # deltan_lsb + "0000000000000000", # cuc_msb + "00000000", # _parity3 + "00", # cuc_lsb + "00000000000000000000", # m0_msb + "00000000", # _parity4 + "000000000000", # m0_lsb + "0000000000", # e_msb + "00000000", # _parity5 + "0000000000000000000000", # e_lsb + "00000000", # _parity6 + "000000000000000000", # cus + "0000", # crc_msb + "00000000", # _parity7 + "00000000000000", # crc_lsb + "00000000", # crs_msb + "00000000", # _parity8 + "0000000000", # crs_lsb + "101001010001", # sqrta_msb + "00000000", # _parity9 + "01010001110110010001", # sqrta_lsb + "01", # toe_msb + "00000000", # _parity10 + ), + BDS_D1_SUBFRAME_2, + ), + ( + ( + "11100010010", # preamble + "0000", # rev + "011", # sid + "00000111", # tow_msb + "0000", # _parity1 + "000000000111", # tow_lsb + "0000000111", # toe_isb + "00000000", # _parity2 + "00111", # toe_lsb + "00000000000000000", # i0_msb + "00000000", # _parity3 + "000000000000000", # i0_lsb + "0000000", # cic_msb + "00000000", # _parity4 + "00000000000", # cic_lsb + "00000000111", # omegadot_msb + "00000000", # _parity5 + "0000000000111", # omegadot_lsb + "000000000", # cis_msb + "00000000", # _parity6 + "000000000", # cis_lsb + "0000000000000", # idot_msb + "00000000", # _parity7 + "0", # idot_lsb + "000000000000000000000", # omega0_msb + "00000000", # _parity8 + "00000000000", # omega0_lsb + "00000000000", # omega_msb + "00000000", # _parity9 + "000000000000000000000", # omega_lsb + "0", # rev1 + "00000000", # _parity10 + ), + BDS_D1_SUBFRAME_3, + ), + ) + raw = RawNav("C", 12, "2I") + for data, dic in BITS: + sfrbits = "0b" + "".join(data) + self.assertEqual(len(sfrbits), 300 + 2) + raw.parse(int(sfrbits, 2), dic, 0b111) + self.assertEqual(raw.gnss, "C") + self.assertEqual(raw.svid, 12) + self.assertEqual(raw.sigcode, "2I") + self.assertEqual(raw.svcode, "C12") + self.assertEqual(raw.identity, "C122I") + self.assertEqual(raw.sid, 3) + self.assertEqual(raw.subframeacq, 7) + self.assertEqual(raw.tow, 0b00000111000000000111) + self.assertEqual(raw.toe, 0b01000000011100111 * 2**3) + self.assertEqual(raw.sqrta, 0b10100101000101010001110110010001 * 2**-19) + self.assertEqual(raw.omegadot, 0b00000000001110000000000111 * 2**-43) + + self.assertEqual(str(raw), EXPECTED_RESULT) + + def testrawnavparserSBA( + self, + ): # test parser produces correct scaled values from subframe bits + EXPECTED_RESULT = "" + BITS = ( + ( + ( + "00000000", # _preamble + "001001", # sid + "00000000", # iodn + "0000000000000", # t0 + "0000", # ura + "000000000000000000000000000111", # xpos + "000000000000000000000000001111", # ypos + "0000000000000000000011111", # zpos + "00000000000000111", # xdot + "00000000000001111", # ydot + "000000000000011111", # zdot + "0000000111", # xdot2 + "0000001111", # ydot2 + "0000011111", # zdot2 + "000000000111", # agf0 + "00001111", # agf1 + "000000000000000000000000", # _parity + ), + SBA_L1CA_MT_9, + ), + ( + ( + "00000000", # _preamble + "001100", # sid + "000000000000000000000111", # a1 + "00000000000000000000000000001111", # a0 + "00000111", # toc + "00000111", # wnt + "00000111", # deltatls + "00000111", # wnlsf + "00000111", # dn + "00000111", # deltatlsf + "011", # utcid + "00000000000000000111", # tow + "0000000111", # wn + "0", # gloind + "00000000000000000000000000000000000000000000000000000000000000000000000111", # deltatglo + "000000000000000000000000", # _parity + ), + SBA_L1CA_MT_12, + ), + ( + ( + "00000000", # _preamble + "010001", # sid + "00", # dataid_01 + "00000000", # prn_01 + "00000000", # svhealth_01 + "000000000000000", # xg_01 + "000000000000000", # yg_01 + "000000000", # zg_01 + "000", # xgdot_01 + "000", # ygdot_01 + "0000", # zgdot_01 + "00", # dataid_02 + "00000000", # prn_02 + "00000000", # svhealth_02 + "000000000000000", # xg_02 + "000000000000000", # yg_02 + "000000000", # zg_02 + "000", # xgdot_02 + "000", # ygdot_02 + "0000", # zgdot_02 + "00", # dataid_03 + "00000000", # prn_03 + "00000000", # svhealth_03 + "000000000000000", # xg_03 + "000000000000000", # yg_03 + "000000000", # zg_03 + "000", # xgdot_03 + "000", # ygdot_03 + "0000", # zgdot_03 + "00000000000", # t0 + "000000000000000000000000", # _parity + ), + SBA_L1CA_MT_17, + ), + ) + raw = RawNav("S", 136, "1C") + for data, dic in BITS: + sfrbits = "0b" + "".join(data) + self.assertEqual(len(sfrbits), 250 + 2) + raw.parse(int(sfrbits, 2), dic, 0b111) + self.assertEqual(raw.gnss, "S") + self.assertEqual(raw.svid, 136) + self.assertEqual(raw.sigcode, "1C") + self.assertEqual(raw.svcode, "S36") + self.assertEqual(raw.identity, "S361C") + self.assertEqual(raw.sid, 17) + self.assertEqual(raw.subframeacq, 7) + self.assertEqual(raw.xpos, 0b111 * .08) + self.assertEqual(raw.ypos, 0b1111 * .08) + self.assertEqual(raw.zpos, 0b11111 * .4) + self.assertEqual(raw.xdot, 0b111 * .000625) + self.assertEqual(raw.ydot, 0b1111 * .000625) + self.assertEqual(raw.zdot, 0b11111 * .004) + self.assertEqual(raw.xdot2, 0b111 * .0000125) + self.assertEqual(raw.ydot2, 0b1111 * .0000125) + self.assertEqual(raw.zdot2, 0b11111 * .0000625) + self.assertEqual(raw.agf0, 0b111 * 2**-31) + self.assertEqual(raw.agf1, 0b1111 * 2**-40) + self.assertEqual(raw.a1, 0b111 * 2**-50) + self.assertEqual(raw.a0, 0b1111 * 2**-30) + self.assertEqual(str(raw), EXPECTED_RESULT) + + # def testrinexnav(self): + # EXPECTED_RESULT_OBS = [ + # r" 3.05 O: OBSERVATION M: MIXED RINEX VERSION / TYPE\n", + # r"PYRINEXCONV 0.1.0 ALSTEVE \b\d{8}\b \b\d{6}\b UTC PGM / RUN BY / DATE\n", + # r"RinexConverter 0.1.0 NAV test COMMENT\n", + # r"LOCAL MARKER NAME\n", + # r"1 MARKER NUMBER\n", + # r"GEODETIC MARKER TYPE\n", + # r"semuadmin OBSERVER / AGENCY\n", + # r"1 ublox X20P HPG 2.02 REC # / TYPE / VERS\n", + # r"1 Beitian BT-184 ANT # / TYPE\n", + # r" 3803648.1838 -148798.4259 5100640.5407 APPROX POSITION XYZ\n", + # r" 0.0000 0.0000 0.0000 ANTENNA: DELTA H/E/N\n", + # r"G 12 C5Q L5Q D5Q S5Q C1C L1C D1C S1C C2L L2L D2L S2L SYS / # / OBS TYPES\n", + # r"C 12 C5P L5P D5P S5P C6C L6C D6C S6C C1P L1P D1P S1P SYS / # / OBS TYPES\n", + # r"E 12 C5Q L5Q D5Q S5Q C1C L1C D1C S1C C6B L6B D6B S6B SYS / # / OBS TYPES\n", + # r"DBHZ SIGNAL STRENGTH UNIT\n", + # r" 1.000 INTERVAL\n", + # r" 2026 4 24 8 47 32.0040000 GPS TIME OF FIRST OBS\n", + # r" 2026 4 24 9 7 19.0030000 GPS TIME OF LAST OBS\n", + # r" GLONASS SLOT / FRQ\n", + # r" C1C 0.000 C1P 0.000 C2C 0.000 C2P 0.000 GLONASS COD/PHS/BIS\n", + # r" 18 2415 5GPS LEAPSECONDS\n", + # r" 34 # OF SATELLITES\n", + # r" END OF HEADER\n", + # r"> 2026 4 24 8 47 32.0040000 0 27 \n", + # r"G26 21437036.766 84123533.8310 209.268 48.000 21437038.61\n", + # r"→4 112652397.0350 280.161 53.000 21437038.650 87781089.8\n", + # r"→610 218.296 44.000 \n", + # r"G27 24519701.570 96220582.5730 2916.127 42.000 24519698.27\n", + # r"→4 128851890.4480 3904.912 40.000 24519702.984 100404084.7\n", + # r"→711 3041.609 24.000 \n", + # ] + + # EXPECTED_RESULT_NAV = [ + # r" 3.05 N: NAVIGATION M: MIXED RINEX VERSION / TYPE\n", + # r"PYRINEXCONV 0.1.0 ALSTEVE \b\d{8}\b \b\d{6}\b UTC PGM / RUN BY / DATE\n", + # r"RinexConverter 0.1.0 NAV test COMMENT\n", + # r"GPSA 1.8626e-08 1.4901e-08 -1.1921e-07 -1.1921e-07 V 29 IONOSPHERIC CORR\n", + # r"GPSB 1.1469e\+05 6.5536e\+04 -1.9661e\+05 -6.5536e\+04 V 29 IONOSPHERIC CORR\n", + # r"GPUT 0.0000000000e\+00-4.656612873e-09 61440 112 G56 0 TIME SYSTEM CORR\n", + # r" 18 2415 5GPS LEAPSECONDS\n", + # r" END OF HEADER\n", + # r"G26 2026 04 24 09 59 42-3.574695438147e-04-4.433786671143e-12 0.000000000000e\+00\n", + # r" 9.300000000000e\+01 2.503125000000e\+01 1.804437488317e-09 5.920644043945e-01\n", + # r" 1.300126314163e-06 1.103244593833e-02 7.566064596176e-06 5.153741914749e\+03\n", + # r" 4.680000000000e\+05-3.576278686523e-07-3.539365250617e-03 4.284083843231e-08\n", + # r" 2.955303755589e-01 2.191875000000e\+02 2.267913562246e-01-2.714386937441e-09\n", + # r" 5.184119800106e-11 3.670000000000e\+02 \n", + # r" 0.000000000000e\+00 0.000000000000e\+00 6.519258022308e-09 \n", + # r" 7.747300000000e\+04 \n", + # ] + + # if RINEXFILETEST is False: + # return + # rc = RinexConverter( + # app="cliapp", + # rinex_version="3.05", + # rinex_types=[""], + # gnssfilter=[""], + # obsfilter=[""], + # datasource=["R", "R", "R"], + # minobs=10, + # marker=["LOCAL", "1", "GEODETIC"], + # antenna=["1", "Beitian BT-184"], + # receiver=["1", "ublox X20P", "HPG 2.02"], + # observer="semuadmin", + # comments=["RinexConverter 0.1.0 NAV test"], + # ) + # rc.process_input("tests/pygpsdata_x20p_rxmsfrbx.log") + # sleep(0.1) + # with open("tests/pygpsdata_R_202604240959_16S_16S_MN.rnx", "r") as infile: + # for i, ln in enumerate(infile.readlines()): + # # print(ln) + # self.assertRegex(ln, EXPECTED_RESULT_NAV[i]) + # if i == 15: + # break + # with open("tests/pygpsdata_R_202604240847_20M_01S_MO.rnx", "r") as infile: + # for i, ln in enumerate(infile.readlines()): + # # print(ln) + # self.assertRegex(ln, EXPECTED_RESULT_OBS[i]) + # if i == 29: + # break