diff --git a/.vscode/settings.json b/.vscode/settings.json index a5a9366..2448ea9 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,5 +4,5 @@ "editor.formatOnSave": true, "modulename": "${workspaceFolderBasename}", "distname": "${workspaceFolderBasename}", - "moduleversion": "1.0.16" + "moduleversion": "1.0.17" } \ No newline at end of file diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index fec79bc..1e2de33 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,5 +1,11 @@ # pyrtcm Release Notes +### RELEASE 1.0.17 + +ENHANCEMENTS: + +1. Add proprietary IGS SSR 4076 messages, as defined in https://files.igs.org/pub/data/format/igs_ssr_v1.pdf. NB not fully tested as available NTRIP sources only cover a subset of the 4076 subtypes defined. + ### RELEASE 1.0.16 CHANGES: diff --git a/pyproject.toml b/pyproject.toml index 7ae518a..05644e7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ name = "pyrtcm" authors = [{ name = "semuadmin", email = "semuadmin@semuconsulting.com" }] maintainers = [{ name = "semuadmin", email = "semuadmin@semuconsulting.com" }] description = "RTCM3 protocol parser" -version = "1.0.16" +version = "1.0.17" license = { file = "LICENSE" } readme = "README.md" requires-python = ">=3.8" @@ -85,7 +85,7 @@ disable = """ [tool.pytest.ini_options] minversion = "7.0" -addopts = "--cov --cov-report term-missing --cov-fail-under 95" +addopts = "--cov --cov-report html --cov-fail-under 98" pythonpath = ["src"] [tool.coverage.run] diff --git a/src/pyrtcm/_version.py b/src/pyrtcm/_version.py index 828f60f..f72a03f 100644 --- a/src/pyrtcm/_version.py +++ b/src/pyrtcm/_version.py @@ -8,4 +8,4 @@ :license: BSD 3-Clause """ -__version__ = "1.0.16" +__version__ = "1.0.17" diff --git a/src/pyrtcm/rtcmmessage.py b/src/pyrtcm/rtcmmessage.py index 81d05e8..d535991 100644 --- a/src/pyrtcm/rtcmmessage.py +++ b/src/pyrtcm/rtcmmessage.py @@ -28,6 +28,8 @@ ATT_NCELL, ATT_NSAT, NCELL, + NHARMCOEFFC, + NHARMCOEFFS, NSAT, NSIG, RTCM_DATA_FIELDS, @@ -133,6 +135,8 @@ def _set_attribute_group(self, att: tuple, offset: int, index: list) -> tuple: if numr == "DF379": numr += f"_{index[-1]:02d}" rng = getattr(self, numr) + if numr == "IDF035": # 4076_201 range is n-1 + rng += 1 index.append(0) # add a (nested) group index level # recursively process each group attribute, @@ -200,6 +204,17 @@ def _set_attribute_single( setattr(self, NSIG, n) elif key == "DF396": # num of cells in MSM message setattr(self, NCELL, n) + # add special coefficient attributes for message 4076_201 + if key == "IDF038": + i = index[0] + N = getattr(self, f"IDF037_{i:02d}") + 1 + M = getattr(self, f"IDF038_{i:02d}") + 1 + nc = int(((N + 1) * (N + 2) / 2) - ((N - M) * (N - M + 1) / 2)) + ns = int(nc - (N + 1)) + # ncs = (N + 1) * (N + 1) - (N - M) * (N - M + 1) + # print(f"DEBUG nc {nc} ns {ns} ncs {ncs} nc+ns {nc+ns}") + setattr(self, NHARMCOEFFC, nc) + setattr(self, NHARMCOEFFS, ns) return offset @@ -338,7 +353,13 @@ def identity(self) -> str: :rtype: str """ - return str(self._payload[0] << 4 | self._payload[1] >> 4) + id = self._payload[0] << 4 | self._payload[1] >> 4 + + if id == 4076: # proprietary IGS SSR message type + subtype = (self._payload[1] & 0x1) << 7 | self._payload[2] >> 1 + id = f"{id}_{subtype:03d}" + + return str(id) @property def payload(self) -> bytes: diff --git a/src/pyrtcm/rtcmtypes_core.py b/src/pyrtcm/rtcmtypes_core.py index 672be0f..6251aa3 100644 --- a/src/pyrtcm/rtcmtypes_core.py +++ b/src/pyrtcm/rtcmtypes_core.py @@ -29,6 +29,8 @@ NSIG = "NSig" NCELL = "NCell" NRES = 16 # number of Residuals groups in MT1023 and MT1024 +NHARMCOEFFC = "_NHarmCoeffC" # number of cosine harmonic coefficients in 4076 +NHARMCOEFFS = "_NHarmCoeffS" # number of sine harmonic coefficients in 4076 # Power of 2 scaling constants P2_P4 = 16 # 2**4 @@ -645,6 +647,49 @@ "DF545": (BIT2, 1, "NAVIC/IRNSS 2 spare bits after i0"), "DF546": (UINT30, 1, "NAVIC/IRNSS Epoch Time (TOW)"), "ExtSatInfo": (UINT4, 0, "Extended Satellite Information"), + # IGS SSR data types, used in 4076 messages + # https://files.igs.org/pub/data/format/igs_ssr_v1.pdf + "IDF001": (UINT3, 1, "IGM/IM Version"), + "IDF002": (UINT8, 1, "IGS Message Number"), + "IDF003": (UINT20, 1, "SSR Epoch Time 1s"), + "IDF004": (BIT4, 1, "SSR Update Interval"), + "IDF005": (BIT1, 1, "SSR Multiple Message Indicator"), + "IDF006": (BIT1, 1, "Global/Regional CRS Indicator"), + "IDF007": (UINT4, 1, "IOD SSR"), + "IDF008": (UINT16, 1, "SSR Provider ID"), + "IDF009": (UINT4, 1, "SSR Solution ID"), + "IDF010": (UINT6, 1, "No. of Satellites"), + "IDF011": (UINT6, 1, "GNSS Satellite ID"), + "IDF012": (BIT8, 1, "GNSS IOD"), + "IDF013": (INT22, 0.1, "Delta Orbit Radial"), + "IDF014": (INT20, 0.4, "Delta Orbit Along-Track"), + "IDF015": (INT20, 0.4, "Delta Orbit Cross-Track"), + "IDF016": (INT21, 0.001, "Dot Orbit Delta Radial"), + "IDF017": (INT19, 0.004, "Dot Orbit Delta Along-Track"), + "IDF018": (INT19, 0.004, "Dot Orbit Delta Cross-Track"), + "IDF019": (INT22, 0.1, "Delta Clock C0"), + "IDF020": (INT21, 0.001, "Delta Clock C1"), + "IDF021": (INT27, 0.00002, "Delta Clock C2"), + "IDF022": (INT22, 0.1, "High Rate Clock Correction"), + "IDF023": (UINT5, 1, "No. of Biases Processed"), + "IDF024": (UINT5, 1, "GNSS Signal and Tracking Mode Identifier"), + "IDF025": (INT14, 0.01, "Code Bias"), + "IDF026": (UINT9, 1 / 256, "Yaw Angle"), + "IDF027": (INT8, 1 / 8192, "Yaw Rate"), + "IDF028": (INT20, 0.0001, "Phase Bias"), + "IDF029": (BIT1, 1, "Signal Integer Indicator"), + "IDF030": (BIT2, 1, "Signals Wide-Lane Integer Indicator"), + "IDF031": (UINT4, 1, "Signal Discontinuity Counter"), + "IDF032": (BIT1, 1, "Dispersive Bias Consistency Indicator"), + "IDF033": (BIT1, 1, "MW Consistency Indicator"), + "IDF034": (BIT6, 1, "SSR URA"), + "IDF035": (UINT2, 1, "Number of Ionospheric Layers"), + "IDF036": (UINT8, 10, "Height of Ionospheric Layer"), + "IDF037": (UINT4, 1, "Spherical Harmonics Degree"), + "IDF038": (UINT4, 1, "Spherical Harmonics Order"), + "IDF039": (INT16, 0.005, "Spherical Harmonic Coefficient C"), + "IDF040": (INT16, 0.005, "Spherical Harmonic Coefficient S"), + "IDF041": (UINT9, 0.05, "VTEC Quality Indicator"), } # *************************************************************************** @@ -786,6 +831,57 @@ "4073": "Unicore Communications Inc", "4075": "Alberding GmbH", "4076": "International GNSS Service (IGS)", + "4076_021": "GPS SSR Orbit Correction", + "4076_022": "GPS SSR Clock Correction", + "4076_023": "GPS SSR Combined Orbit and Clock Correction", + "4076_024": "GPS SSR High Rate Clock Correction", + "4076_025": "GPS SSR Code Bias", + "4076_026": "GPS SSR Phase Bias", + "4076_027": "GPS SSR URA", + # "4076_028-040": "Reserved for GPS", + "4076_041": "GLONASS SSR Orbit Correction", + "4076_042": "GLONASS SSR Clock Correction", + "4076_043": "GLONASS SSR Combined Orbit and Clock Correction", + "4076_044": "GLONASS SSR High Rate Clock Correction", + "4076_045": "GLONASS SSR Code Bias", + "4076_046": "GLONASS SSR Phase Bias", + "4076_047": "GLONASS SSR URA", + # "4076_048-060": "Reserved for GLONASS", + "4076_061": "Galileo SSR Orbit Correction", + "4076_062": "Galileo SSR Clock Correction", + "4076_063": "Galileo SSR Combined Orbit and Clock Correction", + "4076_064": "Galileo SSR High Rate Clock Correction", + "4076_065": "Galileo SSR Code Bias", + "4076_066": "Galileo SSR Phase Bias", + "4076_067": "Galileo SSR URA", + # "4076_068-080": "Reserved for Galileo", + "4076_081": "QZSS SSR Orbit Correction", + "4076_082": "QZSS SSR Clock Correction", + "4076_083": "QZSS SSR Combined Orbit and Clock Correction", + "4076_084": "QZSS SSR High Rate Clock Correction", + "4076_085": "QZSS SSR Code Bias", + "4076_086": "QZSS SSR Phase Bias", + "4076_087": "QZSS SSR URA", + # "4076_088-100": "Reserved for QZSS", + "4076_101": "BeiDou SSR Orbit Correction", + "4076_102": "BeiDou SSR Clock Correction", + "4076_103": "BeiDou SSR Combined Orbit and Clock Correction", + "4076_104": "BeiDou SSR High Rate Clock Correction", + "4076_105": "BeiDou SSR Code Bias", + "4076_106": "BeiDou SSR Phase Bias", + "4076_107": "BeiDou SSR URA", + # "4076_108-120": "Reserved for BeiDou", + "4076_121": "SBAS SSR Orbit Correction", + "4076_122": "SBAS SSR Clock Correction", + "4076_123": "SBAS SSR Combined Orbit and Clock Correction", + "4076_124": "SBAS SSR High Rate Clock Correction", + "4076_125": "SBAS SSR Code Bias", + "4076_126": "SBAS SSR Phase Bias", + "4076_127": "SBAS SSR URA", + # "4076_128-140": "Reserved for SBAS", + # "4076_141-160": "Reserved for NavIC/IRNSS", + # "4076_161-200": "Reserved", + "4076_201": "GNSS SSR Ionosphere VTEC Spherical Harmonics", "4077": "Hemisphere GNSS Inc.", "4078": "ComNav Technology Ltd.", "4079": "SubCarrier Systems Corp. (SCSC) The makers of SNIP", diff --git a/src/pyrtcm/rtcmtypes_get.py b/src/pyrtcm/rtcmtypes_get.py index 0b47853..7349401 100644 --- a/src/pyrtcm/rtcmtypes_get.py +++ b/src/pyrtcm/rtcmtypes_get.py @@ -10,7 +10,7 @@ # pylint: disable=too-many-lines, line-too-long -from pyrtcm.rtcmtypes_core import NCELL, NRES, NSAT +from pyrtcm.rtcmtypes_core import NCELL, NHARMCOEFFC, NHARMCOEFFS, NRES, NSAT # ************************************************************* # MSM MESSAGE SUB-SECTION DEFINITIONS @@ -377,6 +377,184 @@ "DF234": "# of GLONASS Data Entries", } +# ************************************************************* +# RTCM3 IGS SSR 4076 COMMON PAYLOAD DEFINITIONS +# https://files.igs.org/pub/data/format/igs_ssr_v1.pdf +# ************************************************************* +IGM01 = { + "DF002": "RTCM Message Number", + "IDF001": "IGS SSR Version", + "IDF002": "IGS Message Number", + "IDF003": "SSR Epoch Time 1s", + "IDF004": "SSR Update Interval", + "IDF005": "SSR Multiple Message Indicator", + "IDF007": "IOD SSR", + "IDF008": "SSR Provider ID", + "IDF009": "SSR Solution ID", + "IDF006": "Global/Regional CRS Indicator", + "IDF010": "No. of Satellites", + "groupsat": ( + "IDF010", + { + "IDF011": "GNSS Satellite ID", + "IDF012": "GNSS IOD", + "IDF013": "Delta Orbit Radial", + "IDF014": "Delta Orbit Along-Track", + "IDF016": "Delta Orbit Cross-Track", + "IDF015": "Dot Orbit Delta Radial", + "IDF017": "Dot Orbit Delta Along-Track", + "IDF018": "Dot Orbit Delta Cross-Track", + }, + ), +} +IGM02 = { + "DF002": "RTCM Message Number", + "IDF001": "IGS SSR Version", + "IDF002": "IGS Message Number", + "IDF003": "SSR Epoch Time 1s", + "IDF004": "SSR Update Interval", + "IDF005": "SSR Multiple Message Indicator", + "IDF007": "IOD SSR", + "IDF008": "SSR Provider ID", + "IDF009": "SSR Solution ID", + "IDF010": "No. of Satellites", + "groupsat": ( + "IDF010", + { + "IDF011": "GNSS Satellite ID", + "IDF019": "Delta Clock C0", + "IDF020": "Delta Clock C1", + "IDF021": "Delta Clock C2", + }, + ), +} +IGM03 = { + "DF002": "RTCM Message Number", + "IDF001": "IGS SSR Version", + "IDF002": "IGS Message Number", + "IDF003": "SSR Epoch Time 1s", + "IDF004": "SSR Update Interval", + "IDF005": "SSR Multiple Message Indicator", + "IDF007": "IOD SSR", + "IDF008": "SSR Provider ID", + "IDF009": "SSR Solution ID", + "IDF006": "Global/Regional CRS Indicator", + "IDF010": "No. of Satellites", + "groupsat": ( + "IDF010", + { + "IDF011": "GNSS Satellite ID", + "IDF012": "GNSS IOD", + "IDF013": "Delta Orbit Radial", + "IDF014": "Delta Orbit Along-Track", + "IDF015": "Delta Orbit Cross-Track", + "IDF016": "Dot Orbit Delta Radial", + "IDF017": "Dot Orbit Delta Along-Track", + "IDF018": "Dot Orbit Delta Cross-Track", + "IDF019": "Delta Clock C0", + "IDF020": "Delta Clock C1", + "IDF021": "Delta Clock C2", + }, + ), +} +IGM04 = { + "DF002": "RTCM Message Number", + "IDF001": "IGS SSR Version", + "IDF002": "IGS Message Number", + "IDF003": "SSR Epoch Time 1s", + "IDF004": "SSR Update Interval", + "IDF005": "SSR Multiple Message Indicator", + "IDF007": "IOD SSR", + "IDF008": "SSR Provider ID", + "IDF009": "SSR Solution ID", + "IDF010": "No. of Satellites", + "groupsat": ( + "IDF010", + { + "IDF011": "GNSS Satellite ID", + "IDF022": "High Rate Clock Correction", + }, + ), +} +IGM05 = { + "DF002": "RTCM Message Number", + "IDF001": "IGS SSR Version", + "IDF002": "IGS Message Number", + "IDF003": "SSR Epoch Time 1s", + "IDF004": "SSR Update Interval", + "IDF005": "SSR Multiple Message Indicator", + "IDF007": "IOD SSR", + "IDF008": "SSR Provider ID", + "IDF009": "SSR Solution ID", + "IDF010": "No. of Satellites", + "groupsat": ( + "IDF010", + { + "IDF011": "GNSS Satellite ID", + "IDF023": "No. of Biases Processed", + "groupbias": ( + "IDF023", + { + "IDF024": "GNSS Signal and Tracking Mode Identifier", + "IDF025": "Code Bias", + }, + ), + }, + ), +} +IGM06 = { + "DF002": "RTCM Message Number", + "IDF001": "IGS SSR Version", + "IDF002": "IGS Message Number", + "IDF003": "SSR Epoch Time 1s", + "IDF004": "SSR Update Interval", + "IDF005": "SSR Multiple Message Indicator", + "IDF007": "IOD SSR", + "IDF008": "SSR Provider ID", + "IDF009": "SSR Solution ID", + "IDF032": "Dispersive Bias Consistency Indicator", + "IDF033": "MW Consistency Indicator", + "IDF010": "No. of Satellites", + "groupsat": ( + "IDF010", + { + "IDF011": "GNSS Satellite ID", + "IDF023": "No. of Biases Processed", + "IDF026": "Yaw Angle", + "IDF027": "Yaw Rate", + "groupbias": ( + "IDF023", + { + "IDF024": "GNSS Signal and Tracking Mode Identifier", + "IDF029": "Signal Integer Indicator", + "IDF030": "Signals Wide-Lane Integer Indicator", + "IDF031": "Signal Discontinuity Counter", + "IDF028": "Phase Bias", + }, + ), + }, + ), +} +IGM07 = { + "DF002": "RTCM Message Number", + "IDF001": "IGS SSR Version", + "IDF002": "IGS Message Number", + "IDF003": "SSR Epoch Time 1s", + "IDF004": "SSR Update Interval", + "IDF005": "SSR Multiple Message Indicator", + "IDF007": "IOD SSR", + "IDF008": "SSR Provider ID", + "IDF009": "SSR Solution ID", + "IDF010": "No. of Satellites", + "groupsat": ( + "IDF010", + { + "IDF011": "GNSS Satellite ID", + "IDF034": "SSR URA", + }, + ), +} + # ************************************************************* # RTCM3 MESSAGE PAYLOAD DEFINITIONS # ************************************************************* @@ -1837,4 +2015,79 @@ }, ), }, + "4076_021": {**IGM01}, + "4076_022": {**IGM02}, + "4076_023": {**IGM03}, + "4076_024": {**IGM04}, + "4076_025": {**IGM05}, + "4076_026": {**IGM06}, + "4076_027": {**IGM07}, + "4076_041": {**IGM01}, + "4076_042": {**IGM02}, + "4076_043": {**IGM03}, + "4076_044": {**IGM04}, + "4076_045": {**IGM05}, + "4076_046": {**IGM06}, + "4076_047": {**IGM07}, + "4076_061": {**IGM01}, + "4076_062": {**IGM02}, + "4076_063": {**IGM03}, + "4076_064": {**IGM04}, + "4076_065": {**IGM05}, + "4076_066": {**IGM06}, + "4076_067": {**IGM07}, + "4076_081": {**IGM01}, + "4076_082": {**IGM02}, + "4076_083": {**IGM03}, + "4076_084": {**IGM04}, + "4076_085": {**IGM05}, + "4076_086": {**IGM06}, + "4076_087": {**IGM07}, + "4076_101": {**IGM01}, + "4076_102": {**IGM02}, + "4076_103": {**IGM03}, + "4076_104": {**IGM04}, + "4076_105": {**IGM05}, + "4076_106": {**IGM06}, + "4076_107": {**IGM07}, + "4076_121": {**IGM01}, + "4076_122": {**IGM02}, + "4076_123": {**IGM03}, + "4076_124": {**IGM04}, + "4076_125": {**IGM05}, + "4076_126": {**IGM06}, + "4076_127": {**IGM07}, + "4076_201": { + "DF002": "RTCM Message Number", + "IDF001": "IGS SSR Version", + "IDF002": "IGS Message Number", + "IDF003": "SSR Epoch Time 1s", + "IDF004": "SSR Update Interval", + "IDF005": "SSR Multiple Message Indicator", + "IDF007": "IOD SSR", + "IDF008": "SSR Provider ID", + "IDF009": "SSR Solution ID", + "IDF041": "VTEC Quality Indicator", + "IDF035": "Number of Ionospheric Layers", # Nil - 1 + "groupion": ( + "IDF035", + { + "IDF036": "Height of Ionospheric Layer", + "IDF037": "Spherical Harmonics Degree", # N - 1 + "IDF038": "Spherical Harmonics Order", # M - 1 + "groupcoeffC": ( + NHARMCOEFFC, + { + "IDF039": "Spherical Harmonic Coefficient C", + }, + ), + "groupcoeffS": ( + NHARMCOEFFS, + { + "IDF040": "Spherical Harmonic Coefficient S", + }, + ), + }, + ), + }, } diff --git a/tests/pygpsdata-NTRIP-4076.log b/tests/pygpsdata-NTRIP-4076.log new file mode 100644 index 0000000..9712bdf Binary files /dev/null and b/tests/pygpsdata-NTRIP-4076.log differ diff --git a/tests/test_stream.py b/tests/test_stream.py index 5a60601..0eef9fb 100644 --- a/tests/test_stream.py +++ b/tests/test_stream.py @@ -275,6 +275,27 @@ def testntrip2log( self.assertEqual(f"{parsed}", EXPECTED_RESULTS[i]) i += 1 + def testigsssr4076( + self, + ): # test 4076 messages using log from NTRIP caster products.igs-ip.net, mountpoint SSRA00CNE1 + EXPECTED_RESULTS = [ + "", + "", + "", + "", + "", + ] + dirname = os.path.dirname(__file__) + with open(os.path.join(dirname, "pygpsdata-NTRIP-4076.log"), "rb") as stream: + i = 0 + raw = 0 + rtr = RTCMReader(stream, scaling=True, labelmsm=True) + for raw, parsed in rtr: + if raw is not None: + # print(f'"{parsed}",') + self.assertEqual(f"{parsed}", EXPECTED_RESULTS[i]) + i += 1 + def testSerialize(self): # test serialize() payload = self._raw1005[3:-3] msg1 = RTCMReader.parse(self._raw1005)