From 41d541a5903721a392632bbad2a1ec75a265252a Mon Sep 17 00:00:00 2001 From: semuadmin <28569967+semuadmin@users.noreply.github.com> Date: Sun, 12 May 2024 15:42:02 +0100 Subject: [PATCH 1/7] streamline prn and sig maps --- pyproject.toml | 2 +- src/pyrtcm/rtcmhelpers.py | 6 +- src/pyrtcm/rtcmmessage.py | 69 ++++++++++++--- src/pyrtcm/rtcmtables.py | 163 ----------------------------------- src/pyrtcm/rtcmtypes_core.py | 4 + src/pyrtcm/rtcmtypes_get.py | 66 ++++++++++++++ tests/test_stream.py | 71 ++++++++------- 7 files changed, 167 insertions(+), 214 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 35e699a..4009d43 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -85,7 +85,7 @@ disable = """ [tool.pytest.ini_options] minversion = "7.0" -addopts = "--cov --cov-report html --cov-fail-under 98" +addopts = "--cov --cov-report html --cov-fail-under 90" pythonpath = ["src"] [tool.coverage.run] diff --git a/src/pyrtcm/rtcmhelpers.py b/src/pyrtcm/rtcmhelpers.py index a0dd6e7..3f13d40 100644 --- a/src/pyrtcm/rtcmhelpers.py +++ b/src/pyrtcm/rtcmhelpers.py @@ -279,7 +279,7 @@ def sat2prn(msg: object) -> dict: for idx in range(1, 65): if msg.DF394 & 2 ** (64 - idx): nsat += 1 - sats[nsat] = prnmap[idx] + sats[nsat] = prnmap.get(idx, "Reserved") return sats @@ -314,14 +314,14 @@ def cell2prn(msg: object, sigcode: int = 1) -> dict: nsat = 0 for idx in range(1, 65): if msg.DF394 & 2 ** (64 - idx): - sats.append(prnmap[idx]) + sats.append(prnmap.get(idx, "Reserved")) nsat += 1 sigs = [] nsig = 0 for idx in range(1, 33): if msg.DF395 & 2 ** (32 - idx): - sgc = sigmap[idx] + sgc = sigmap.get(idx, "Reserved") fqc = sgc[1] if sigcode else sgc[0] sigs.append(fqc) nsig += 1 diff --git a/src/pyrtcm/rtcmmessage.py b/src/pyrtcm/rtcmmessage.py index 576014c..e05e25c 100644 --- a/src/pyrtcm/rtcmmessage.py +++ b/src/pyrtcm/rtcmmessage.py @@ -15,20 +15,21 @@ att2name, attsiz, bits2val, - cell2prn, crc2bytes, escapeall, len2bytes, - sat2prn, ) +from pyrtcm.rtcmtables import PRNSIGMAP from pyrtcm.rtcmtypes_core import ( ATT_NCELL, ATT_NSAT, + CEL, NCELL, NHARMCOEFFC, NHARMCOEFFS, NSAT, NSIG, + PRN, RTCM_DATA_FIELDS, RTCM_HDR, RTCM_MSGIDS, @@ -59,6 +60,8 @@ def __init__(self, payload: bytes = None, scaling: bool = True, labelmsm: int = self._scaling = scaling self._labelmsm = labelmsm self._unknown = False + self._satmap = None + self._cellmap = None self._do_attributes() self._immutable = True # once initialised, object is immutable @@ -215,8 +218,13 @@ def _set_attribute_single( asiz = getattr(self, NSAT) * getattr(self, NSIG) else: asiz = attsiz(atyp) - bitfield = self._getbits(offset, asiz) - val = bits2val(atyp, ares, bitfield) + if atyp == PRN: + val = self._satmap[index[0]] + elif atyp == CEL: + val = self._cellmap[index[0]] + else: + bitfield = self._getbits(offset, asiz) + val = bits2val(atyp, ares, bitfield) setattr(self, anami, val) offset += asiz @@ -234,6 +242,9 @@ def _set_attribute_single( setattr(self, NSIG, nbits) elif anam == "DF396": # num of cells in MSM message setattr(self, NCELL, nbits) + # populate NSAT and NCELL mapping dictionaries + self._getsatcellmaps() + # add special coefficient attributes for message 4076_201 if anam == "IDF038": i = index[0] @@ -248,6 +259,44 @@ def _set_attribute_single( return offset + def _getsatcellmaps(self): + """ + Map group indices to satellite PRN & signal ID values via + bitmasks DF394, DF395 and DF396. + """ + + prnmap, sigmap = PRNSIGMAP[str(self.identity)[0:3]] + sigcode = 0 if self._labelmsm == 2 else 1 + + sats = {} + nsat = 0 + for idx in range(1, 65): + if getattr(self, "DF394") & 2 ** (64 - idx): + nsat += 1 + sats[nsat] = prnmap.get(idx, "Reserved") + + sigs = [] + nsig = 0 + for idx in range(1, 33): + if getattr(self, "DF395") & 2 ** (32 - idx): + sgc = sigmap.get(idx, "Reserved") + fqc = sgc[1] if sigcode else sgc[0] + sigs.append(fqc) + nsig += 1 + + ncells = int(nsat * nsig) + cells = {} + ncell = idx = 0 + for sat in range(nsat): + for sig in range(nsig): + idx += 1 + if getattr(self, "DF396") & 2 ** (ncells - idx): + ncell += 1 + cells[ncell] = (sats[sat + 1], sigs[sig]) + + self._satmap = sats + self._cellmap = cells + def _getbits(self, position: int, length: int) -> int: """ Get unsigned integer value of masked bits in bytes. @@ -295,14 +344,6 @@ def __str__(self) -> str: :rtype: str """ - # if MSM message and labelmsm flag is set, - # label NSAT and NCELL group attributes with - # corresponding satellite PRN and signal ID (RINEX code or freq band) - if not self._unknown: - if self._labelmsm and self.ismsm: - sats = sat2prn(self) - cells = cell2prn(self, 0 if self._labelmsm == 2 else 1) - stg = f" str: if self._labelmsm and self.ismsm: aname = att2name(att) if aname in ATT_NSAT: - prn = sats[att2idx(att)] + prn = self._satmap[att2idx(att)] lbl = f"({prn})" if aname in ATT_NCELL: - prn, sig = cells[att2idx(att)] + prn, sig = self._cellmap[att2idx(att)] lbl = f"({prn},{sig})" stg += att + lbl + "=" + str(val) diff --git a/src/pyrtcm/rtcmtables.py b/src/pyrtcm/rtcmtables.py index dd95bc2..4f1fc98 100644 --- a/src/pyrtcm/rtcmtables.py +++ b/src/pyrtcm/rtcmtables.py @@ -15,38 +15,20 @@ GPS_PRN_MAP = {} for i in range(1, 64): GPS_PRN_MAP[i] = f"{i:03d}" -GPS_PRN_MAP[64] = "Reserved" GPS_SIG_MAP = { - 1: "Reserved", 2: ("L1", "1C"), 3: ("L1", "1P"), 4: ("L1", "1W"), - 5: "Reserved", - 6: "Reserved", - 7: "Reserved", 8: ("L2", "2C"), 9: ("L2", "2P"), 10: ("L2", "2W"), - 11: "Reserved", - 12: "Reserved", - 13: "Reserved", - 14: "Reserved", 15: ("L2", "2S"), 16: ("L2", "2L"), 17: ("L2", "2X"), - 18: "Reserved", - 19: "Reserved", - 20: "Reserved", - 21: "Reserved", 22: ("L5", "5I"), 23: ("L5", "5Q"), 24: ("L5", "5X"), - 25: "Reserved", - 26: "Reserved", - 27: "Reserved", - 28: "Reserved", - 29: "Reserved", 30: ("L1", "1S"), 31: ("L1", "1L"), 32: ("L1", "1X"), @@ -59,42 +41,12 @@ GLONASS_PRN_MAP = {} for i in range(1, 25): GLONASS_PRN_MAP[i] = f"{i:03d}" -for i in range(25, 65): - GLONASS_PRN_MAP[i] = "Reserved" GLONASS_SIG_MAP = { - 1: "Reserved", 2: ("G1", "1C"), 3: ("G1", "1P"), - 4: "Reserved", - 5: "Reserved", - 6: "Reserved", - 7: "Reserved", 8: ("G2", "2C"), 9: ("G2", "2P"), - 10: "Reserved", - 11: "Reserved", - 12: "Reserved", - 13: "Reserved", - 14: "Reserved", - 15: "Reserved", - 16: "Reserved", - 17: "Reserved", - 18: "Reserved", - 19: "Reserved", - 20: "Reserved", - 21: "Reserved", - 22: "Reserved", - 23: "Reserved", - 24: "Reserved", - 25: "Reserved", - 26: "Reserved", - 27: "Reserved", - 28: "Reserved", - 29: "Reserved", - 30: "Reserved", - 31: "Reserved", - 32: "Reserved", } ################################################# @@ -106,42 +58,27 @@ GALILEO_PRN_MAP[i] = f"{i:03d}" GALILEO_PRN_MAP[51] = "GIOVE-A" GALILEO_PRN_MAP[52] = "GIOVE-B" -for i in range(53, 65): - GALILEO_PRN_MAP[i] = "Reserved" GALILEO_SIG_MAP = { - 1: "Reserved", 2: ("E1", "1C"), 3: ("E1", "1A"), 4: ("E1", "1B"), 5: ("E1", "1X"), 6: ("E1", "1Z"), - 7: "Reserved", 8: ("E6", "6C"), 9: ("E6", "6A"), 10: ("E6", "6B"), 11: ("E6", "6X"), 12: ("E6", "6Z"), - 13: "Reserved", 14: ("E5B", "7I"), 15: ("E5B", "7Q"), 16: ("E5B", "7X"), - 17: "Reserved", 18: ("E5AB", "8I"), 19: ("E5AB", "8Q"), 20: ("E5AB", "8X"), - 21: "Reserved", 22: ("E5A", "5I"), 23: ("E5A", "5Q"), 24: ("E5A", "5X"), - 25: "Reserved", - 26: "Reserved", - 27: "Reserved", - 28: "Reserved", - 29: "Reserved", - 30: "Reserved", - 31: "Reserved", - 32: "Reserved", } ################################################# @@ -151,42 +88,12 @@ SBAS_PRN_MAP = {} for i in range(1, 40): SBAS_PRN_MAP[i] = f"{i+119:03d}" -for i in range(40, 65): - SBAS_PRN_MAP[i] = "Reserved" SBAS_SIG_MAP = { - 1: "Reserved", 2: ("L1", "1C"), - 3: "Reserved", - 4: "Reserved", - 5: "Reserved", - 6: "Reserved", - 7: "Reserved", - 8: "Reserved", - 9: "Reserved", - 10: "Reserved", - 11: "Reserved", - 12: "Reserved", - 13: "Reserved", - 14: "Reserved", - 15: "Reserved", - 16: "Reserved", - 17: "Reserved", - 18: "Reserved", - 19: "Reserved", - 20: "Reserved", - 21: "Reserved", 22: ("L5", "5I"), 23: ("L5", "5Q"), 24: ("L5", "5X"), - 25: "Reserved", - 26: "Reserved", - 27: "Reserved", - 28: "Reserved", - 29: "Reserved", - 30: "Reserved", - 31: "Reserved", - 32: "Reserved", } ################################################# @@ -196,39 +103,18 @@ QZSS_PRN_MAP = {} for i in range(1, 11): QZSS_PRN_MAP[i] = f"{i+192:03d}" -for i in range(11, 65): - QZSS_PRN_MAP[i] = "Reserved" QZSS_SIG_MAP = { - 1: "Reserved", 2: ("L1", "1C"), - 3: "Reserved", - 4: "Reserved", - 5: "Reserved", - 6: "Reserved", - 7: "Reserved", - 8: "Reserved", 9: ("LEX", "6S"), 10: ("LEX", "6L"), 11: ("LEX", "6X"), - 12: "Reserved", - 13: "Reserved", - 14: "Reserved", 15: ("L2", "2S"), 16: ("L2", "2L"), 17: ("L2", "2X"), - 18: "Reserved", - 19: "Reserved", - 20: "Reserved", - 21: "Reserved", 22: ("L5", "5I"), 23: ("L5", "5Q"), 24: ("L5", "5X"), - 25: "Reserved", - 26: "Reserved", - 27: "Reserved", - 28: "Reserved", - 29: "Reserved", 30: ("L1", "1S"), 31: ("L1", "1L"), 32: ("L1", "1X"), @@ -241,35 +127,19 @@ BEIDOU_PRN_MAP = GPS_PRN_MAP BEIDOU_SIG_MAP = { - 1: "Reserved", 2: ("B1", "2I"), 3: ("B1", "2Q"), 4: ("B1", "2X"), - 5: "Reserved", - 6: "Reserved", - 7: "Reserved", 8: ("B3", "6I"), 9: ("B3", "6Q"), 10: ("B3", "6X"), - 11: "Reserved", - 12: "Reserved", - 13: "Reserved", 14: ("B2", "7I"), 15: ("B2", "7Q"), 16: ("B2", "7X"), - 17: "Reserved", - 18: "Reserved", - 19: "Reserved", - 20: "Reserved", - 21: "Reserved", 22: ("B2A", "5D"), 23: ("B2A", "5P"), 24: ("B2A", "5X"), 25: ("B2A", "7D"), - 26: "Reserved", - 27: "Reserved", - 28: "Reserved", - 29: "Reserved", 30: ("B1C", "1D"), 31: ("B1C", "1P"), 32: ("B1C", "1X"), @@ -282,42 +152,9 @@ IRNSS_PRN_MAP = {} for i in range(1, 15): IRNSS_PRN_MAP[i] = f"{i:03d}" -for i in range(15, 65): - IRNSS_PRN_MAP[i] = "Reserved" IRNSS_SIG_MAP = { - 1: "Reserved", - 2: "Reserved", - 3: "Reserved", - 4: "Reserved", - 5: "Reserved", - 6: "Reserved", - 7: "Reserved", - 8: "Reserved", - 9: "Reserved", - 10: "Reserved", - 11: "Reserved", - 12: "Reserved", - 13: "Reserved", - 14: "Reserved", - 15: "Reserved", - 16: "Reserved", - 17: "Reserved", - 18: "Reserved", - 19: "Reserved", - 20: "Reserved", - 21: "Reserved", 22: ("L5", "5A"), - 23: "Reserved", - 24: "Reserved", - 25: "Reserved", - 26: "Reserved", - 27: "Reserved", - 28: "Reserved", - 29: "Reserved", - 30: "Reserved", - 31: "Reserved", - 32: "Reserved", } # {identity[0:3]: (prnmap, sigmap)} diff --git a/src/pyrtcm/rtcmtypes_core.py b/src/pyrtcm/rtcmtypes_core.py index c4cb06c..74efb5e 100644 --- a/src/pyrtcm/rtcmtypes_core.py +++ b/src/pyrtcm/rtcmtypes_core.py @@ -154,6 +154,8 @@ INTS27 = "SNT027" # 27 bit sign-magnitude integer INTS32 = "SNT032" # 32 bit sign-magnitude integer UTF8 = "UTF008" # Unicode UTF-8 Code Unit +PRN = "PRN000" # Derived satellite PRN +CEL = "CEL000" # Derived cell (prn/signal) # **************************************************** @@ -167,6 +169,8 @@ # # **************************************************** RTCM_DATA_FIELDS = { + "PRN": (PRN, 0, "Derived satellite PRN"), + "CELL": (CEL, 0, "Derived satellite PRN & signal ID"), "DF001": (BIT1, 0, "Reserved Field"), "DF001_1": (BIT1, 0, "Reserved 1 bit"), "DF001_2": (BIT2, 0, "Reserved 2 bits"), diff --git a/src/pyrtcm/rtcmtypes_get.py b/src/pyrtcm/rtcmtypes_get.py index 59080a2..1500df7 100644 --- a/src/pyrtcm/rtcmtypes_get.py +++ b/src/pyrtcm/rtcmtypes_get.py @@ -77,6 +77,12 @@ } MSM_SAT_123 = { + "groupsat0": ( + NSAT, + { + "PRN": "GNSS Satellite PRN", + }, + ), "groupsat1": ( NSAT, { @@ -86,6 +92,12 @@ } MSM_SAT_46 = { + "groupsat0": ( + NSAT, + { + "PRN": "GNSS Satellite PRN", + }, + ), "groupsat1": ( NSAT, { @@ -102,6 +114,12 @@ MSM_SAT_57 = { + "groupsat0": ( + NSAT, + { + "PRN": "GNSS Satellite PRN", + }, + ), "groupsat1": ( NSAT, { @@ -129,6 +147,12 @@ } MSM_SAT_57_GLONASS = { + "groupsat0": ( + NSAT, + { + "PRN": "GNSS Satellite PRN", + }, + ), "groupsat1": ( NSAT, { @@ -156,6 +180,12 @@ } MSM_SIG_1 = { + "groupsig0": ( + NCELL, + { + "CELL": "GNSS Satellite PRN & Signal ID", + }, + ), "groupsig1": ( NCELL, { @@ -165,6 +195,12 @@ } MSM_SIG_2 = { + "groupsig0": ( + NCELL, + { + "CELL": "GNSS Satellite PRN & Signal ID", + }, + ), "groupsig1": ( NCELL, { @@ -187,6 +223,12 @@ } MSM_SIG_3 = { + "groupsig0": ( + NCELL, + { + "CELL": "GNSS Satellite PRN & Signal ID", + }, + ), "groupsig1": ( NCELL, { @@ -214,6 +256,12 @@ } MSM_SIG_4 = { + "groupsig0": ( + NCELL, + { + "CELL": "GNSS Satellite PRN & Signal ID", + }, + ), "groupsig1": ( NCELL, { @@ -247,6 +295,12 @@ } MSM_SIG_5 = { + "groupsig0": ( + NCELL, + { + "CELL": "GNSS Satellite PRN & Signal ID", + }, + ), "groupsig1": ( NCELL, { @@ -286,6 +340,12 @@ } MSM_SIG_6 = { + "groupsig0": ( + NCELL, + { + "CELL": "GNSS Satellite PRN & Signal ID", + }, + ), "groupsig1": ( NCELL, { @@ -319,6 +379,12 @@ } MSM_SIG_7 = { + "groupsig0": ( + NCELL, + { + "CELL": "GNSS Satellite PRN & Signal ID", + }, + ), "groupsig1": ( NCELL, { diff --git a/tests/test_stream.py b/tests/test_stream.py index 594168e..99a4650 100644 --- a/tests/test_stream.py +++ b/tests/test_stream.py @@ -103,9 +103,9 @@ def testMSM3( dirname = os.path.dirname(__file__) with open(os.path.join(dirname, "pygpsdata-RTCMMSM3.log"), "rb") as stream: EXPECTED_RESULT = ( - "", - "", - "", + "", + "", + "", ) rtr = RTCMReader(stream, quitonerror=2) i = 0 @@ -121,13 +121,13 @@ def testMIXEDRTCM_NOSCALE( EXPECTED_RESULTS = ( "", "", - "", - "", - "", - "", + "", + "", + "", + "", "", "", - "", + "", ) dirname = os.path.dirname(__file__) @@ -140,6 +140,7 @@ def testMIXEDRTCM_NOSCALE( # print(f'"{parsed}",') self.assertEqual(str(parsed), EXPECTED_RESULTS[i]) i += 1 + self.assertEqual(i, 9) def testMIXEDRTCM_SCALE( self, @@ -147,13 +148,13 @@ def testMIXEDRTCM_SCALE( EXPECTED_RESULTS = ( "", "", - "", - "", - "", - "", + "", + "", + "", + "", "", "", - "", + "", ) dirname = os.path.dirname(__file__) with open(os.path.join(dirname, "pygpsdata-RTCM3.log"), "rb") as stream: @@ -165,6 +166,7 @@ def testMIXEDRTCM_SCALE( # print(f'"{parsed}",') self.assertEqual(str(parsed), EXPECTED_RESULTS[i]) i += 1 + self.assertEqual(i, 9) def testMIXEDRTCM_SCALE_LABELMSM_RINEX( self, @@ -172,13 +174,13 @@ def testMIXEDRTCM_SCALE_LABELMSM_RINEX( EXPECTED_RESULTS = ( "", "", - "", - "", - "", - "", + "", + "", + "", + "", "", "", - "", + "", ) dirname = os.path.dirname(__file__) @@ -193,6 +195,7 @@ def testMIXEDRTCM_SCALE_LABELMSM_RINEX( # print(f'"{parsed}",') self.assertEqual(str(parsed), EXPECTED_RESULTS[i]) i += 1 + self.assertEqual(i, 9) def testMIXEDRTCM_SCALE_LABELMSM_FREQ( self, @@ -200,13 +203,13 @@ def testMIXEDRTCM_SCALE_LABELMSM_FREQ( EXPECTED_RESULTS = ( "", "", - "", - "", - "", - "", + "", + "", + "", + "", "", "", - "", + "", ) dirname = os.path.dirname(__file__) @@ -221,6 +224,7 @@ def testMIXEDRTCM_SCALE_LABELMSM_FREQ( # print(f'"{parsed}",') self.assertEqual(str(parsed), EXPECTED_RESULTS[i]) i += 1 + self.assertEqual(i, 9) def testntrip2log( self, @@ -244,18 +248,18 @@ def testntrip2log( "", "", "", - "", - "", - "", - "", - "", - "", - "", - "", + "", + "", + "", + "", + "", + "", + "", + "", "", "", - "", - "", + "", + "", "", "", "", @@ -274,6 +278,7 @@ def testntrip2log( # print(f'"{parsed}",') self.assertEqual(f"{parsed}", EXPECTED_RESULTS[i]) i += 1 + self.assertEqual(i, 35) def testigsssr4076( self, From 674c28a12f17480996f5973811689a51d564ff05 Mon Sep 17 00:00:00 2001 From: semuadmin <28569967+semuadmin@users.noreply.github.com> Date: Sun, 12 May 2024 17:08:36 +0100 Subject: [PATCH 2/7] remove sat2prn and cell2prn helper methods --- README.md | 4 +- pyproject.toml | 2 +- src/pyrtcm/rtcmhelpers.py | 111 +++---------------- src/pyrtcm/rtcmmessage.py | 36 ++---- src/pyrtcm/rtcmreader.py | 4 +- src/pyrtcm/rtcmtypes_core.py | 22 +--- src/pyrtcm/rtcmtypes_get.py | 21 ++-- tests/test_specialcases.py | 207 ----------------------------------- tests/test_static.py | 68 ++++++------ tests/test_stream.py | 70 ++++++------ 10 files changed, 117 insertions(+), 428 deletions(-) delete mode 100644 tests/test_specialcases.py diff --git a/README.md b/README.md index ff0ced5..c60df8a 100644 --- a/README.md +++ b/README.md @@ -154,13 +154,13 @@ print(msg.DF034) print(msg.DF419_03) ``` ``` - + '1087' 42119001 8 ``` -Attributes within repeating groups are parsed with a two-digit suffix (`DF419_01`, `DF419_02`, etc. See [example below](#iterating) for an illustration of how to iterate through grouped attributes). Attributes within MSM NSAT and NCELL repeating groups can optionally be labelled with their corresponding satellite PRN and signal ID when the `__str__()` (`print()`) method is invoked, by setting the keyword argument `labelmsm` to True - e.g. `DF404_13(023,1C)` signifies that the 13th item in the DF404 ("fine Phase Range Rate") group refers to satellite PRN 023, signal ID 1C. +Attributes within repeating groups are parsed with a two-digit suffix (`DF419_01`, `DF419_02`, etc. See [example below](#iterating) for an illustration of how to iterate through grouped attributes). Helper methods are available to interpret the individual datafields: diff --git a/pyproject.toml b/pyproject.toml index 4009d43..35e699a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -85,7 +85,7 @@ disable = """ [tool.pytest.ini_options] minversion = "7.0" -addopts = "--cov --cov-report html --cov-fail-under 90" +addopts = "--cov --cov-report html --cov-fail-under 98" pythonpath = ["src"] [tool.coverage.run] diff --git a/src/pyrtcm/rtcmhelpers.py b/src/pyrtcm/rtcmhelpers.py index 3f13d40..72df57e 100644 --- a/src/pyrtcm/rtcmhelpers.py +++ b/src/pyrtcm/rtcmhelpers.py @@ -11,9 +11,7 @@ from datetime import datetime, timedelta -from pyrtcm.exceptions import RTCMTypeError -from pyrtcm.rtcmtables import PRNSIGMAP -from pyrtcm.rtcmtypes_core import ATT_NCELL, ATT_NSAT, COEFFS, GNSSMAP, RTCM_DATA_FIELDS +from pyrtcm.rtcmtypes_core import COEFFS, GNSSMAP, RTCM_DATA_FIELDS def att2idx(att: str) -> int: @@ -258,92 +256,6 @@ def hextable(raw: bytes, cols: int = 8) -> str: return hextbl -def sat2prn(msg: object) -> dict: - """ - Map MSM sat to satellite PRN for a given RTCM3 MSM message. - - Returns a dict mapping the satellite PRN corresponding to each - item in the MSM NSAT repeating group e.g. DF397_01, DF397_02, etc. - - :param RTCMMessage msg: RTCM3 MSM message e.g. 1077 - :return: dict of {cell: prn} values - :rtype: dict - :raises: RTCMTypeError if not MSM message type - """ - - try: - prnmap, _ = PRNSIGMAP[str(msg.identity)[0:3]] - - sats = {} - nsat = 0 - for idx in range(1, 65): - if msg.DF394 & 2 ** (64 - idx): - nsat += 1 - sats[nsat] = prnmap.get(idx, "Reserved") - - return sats - - except (TypeError, KeyError, AttributeError) as err: - raise RTCMTypeError( - "Invalid RTCM3 message type - must be MSM message." - ) from err - - -def cell2prn(msg: object, sigcode: int = 1) -> dict: - """ - Map MSM cell to satellite PRN and signal ID for a given RTCM3 MSM message. - - Returns a dict mapping the satellite PRN and signal ID corresponding to each - item in the MSM NCELL repeating group e.g. DF405_01, DF406_02, etc. - - DF394 bitmask indicates which satellites are present. - DF395 bitmask indicates which signals are present. - DF396 bitmask maps satellite to signal. - - :param RTCMMessage msg: RTCM3 MSM message e.g. 1077 - :param int sigcode: 0 = use frequency band, 1 = use signal RINEX code - :return: dict of {cell: (prn, sig)} values - :rtype: dict - :raises: RTCMTypeError if not MSM message type - """ - - try: - prnmap, sigmap = PRNSIGMAP[str(msg.identity)[0:3]] - - sats = [] - nsat = 0 - for idx in range(1, 65): - if msg.DF394 & 2 ** (64 - idx): - sats.append(prnmap.get(idx, "Reserved")) - nsat += 1 - - sigs = [] - nsig = 0 - for idx in range(1, 33): - if msg.DF395 & 2 ** (32 - idx): - sgc = sigmap.get(idx, "Reserved") - fqc = sgc[1] if sigcode else sgc[0] - sigs.append(fqc) - nsig += 1 - - ncells = int(nsat * nsig) - cells = {} - ncell = idx = 0 - for sat in range(nsat): - for sig in range(nsig): - idx += 1 - if msg.DF396 & 2 ** (ncells - idx): - ncell += 1 - cells[ncell] = (sats[sat], sigs[sig]) - - return cells - - except (TypeError, KeyError, AttributeError) as err: - raise RTCMTypeError( - "Invalid RTCM3 message type - must be MSM message." - ) from err - - def escapeall(val: bytes) -> str: """ Escape all byte characters e.g. b'\\\\x73' rather than b`s` @@ -368,8 +280,6 @@ def parse_msm(msg: object) -> tuple: if not msg.ismsm: return None - satmap = sat2prn(msg) # maps indices to satellite PRNs - cellmap = cell2prn(msg) # maps indices to cells (satellite PRN, signal ID) meta = {} gmap = GNSSMAP[msg.identity[0:3]] meta["identity"] = msg.identity @@ -381,16 +291,27 @@ def parse_msm(msg: object) -> tuple: msmsats = [] for i in range(1, msg.NSat + 1): # iterate through satellites sats = {} - sats["PRN"] = satmap[i] - for attr in ATT_NSAT: + for attr in ["PRN", "DF397", "DF398", "DF399", "DF419", "ExtSatInfo"]: if hasattr(msg, f"{attr}_{i:02d}"): sats[attr] = getattr(msg, f"{attr}_{i:02d}") msmsats.append(sats) msmcells = [] for i in range(1, msg.NCell + 1): # iterate through cells (satellite/signal) cells = {} - cells["PRN"], cells["SIGNAL"] = cellmap[i] - for attr in ATT_NCELL: + for attr in [ + "CELLPRN", + "CELLSIG", + "DF400", + "DF401", + "DF402", + "DF403", + "DF404", + "DF405", + "DF406", + "DF407", + "DF408", + "DF420", + ]: if hasattr(msg, f"{attr}_{i:02d}"): cells[attr] = getattr(msg, f"{attr}_{i:02d}") msmcells.append(cells) diff --git a/src/pyrtcm/rtcmmessage.py b/src/pyrtcm/rtcmmessage.py index e05e25c..4be1270 100644 --- a/src/pyrtcm/rtcmmessage.py +++ b/src/pyrtcm/rtcmmessage.py @@ -10,20 +10,11 @@ import pyrtcm.exceptions as rte import pyrtcm.rtcmtypes_get as rtg -from pyrtcm.rtcmhelpers import ( - att2idx, - att2name, - attsiz, - bits2val, - crc2bytes, - escapeall, - len2bytes, -) +from pyrtcm.rtcmhelpers import attsiz, bits2val, crc2bytes, escapeall, len2bytes from pyrtcm.rtcmtables import PRNSIGMAP from pyrtcm.rtcmtypes_core import ( - ATT_NCELL, - ATT_NSAT, - CEL, + CELPRN, + CELSIG, NCELL, NHARMCOEFFC, NHARMCOEFFS, @@ -46,7 +37,7 @@ def __init__(self, payload: bytes = None, scaling: bool = True, labelmsm: int = :param bytes payload: message payload (mandatory) :param bool scaling: whether to apply attribute scaling True/False (True) - :param int labelmsm: MSM NSAT and NCELL attribute label (0 = none, 1 = RINEX, 2 = freq) + :param int labelmsm: MSM NSAT and NCELL attribute label (1 = RINEX, 2 = freq) :raises: RTCMMessageError """ @@ -220,8 +211,10 @@ def _set_attribute_single( asiz = attsiz(atyp) if atyp == PRN: val = self._satmap[index[0]] - elif atyp == CEL: - val = self._cellmap[index[0]] + elif atyp == CELPRN: + val = self._cellmap[index[0]][0] + elif atyp == CELSIG: + val = self._cellmap[index[0]][1] else: bitfield = self._getbits(offset, asiz) val = bits2val(atyp, ares, bitfield) @@ -351,18 +344,7 @@ def __str__(self) -> str: # escape all byte chars if isinstance(val, bytes): val = escapeall(val) - # label MSM NSAT and NCELL group attributes - lbl = "" - if self._labelmsm and self.ismsm: - aname = att2name(att) - if aname in ATT_NSAT: - prn = self._satmap[att2idx(att)] - lbl = f"({prn})" - if aname in ATT_NCELL: - prn, sig = self._cellmap[att2idx(att)] - lbl = f"({prn},{sig})" - - stg += att + lbl + "=" + str(val) + stg += att + "=" + str(val) if i < len(self.__dict__) - 1: stg += ", " if self._unknown: diff --git a/src/pyrtcm/rtcmreader.py b/src/pyrtcm/rtcmreader.py index 92d5830..347ab8b 100644 --- a/src/pyrtcm/rtcmreader.py +++ b/src/pyrtcm/rtcmreader.py @@ -54,7 +54,7 @@ def __init__( :param int validate: 0 = ignore invalid checksum, 1 = validate checksum (1) :param int quitonerror: 0 = ignore, 1 = log and continue, 2 = (re)raise (1) :param bool scaling: apply attribute scaling True/False (True) - :param int labelmsm: MSM NSAT and NCELL attribute label (0 = none, 1 = RINEX, 2 = freq) + :param int labelmsm: MSM NSAT and NCELL attribute label (1 = RINEX, 2 = freq) :param int bufsize: socket recv buffer size (4096) :param object errorhandler: error handling object or function (None) :raises: RTCMStreamError (if mode is invalid) @@ -272,7 +272,7 @@ def parse( :param bytes message: RTCM raw message bytes :param int validate: 0 = don't validate CRC, 1 = validate CRC (1) :param bool scaling: apply attribute scaling True/False (True) - :param int labelmsm: MSM NSAT and NCELL attribute label (0 = none, 1 = RINEX, 2 = freq) + :param int labelmsm: MSM NSAT and NCELL attribute label (1 = RINEX, 2 = freq) :return: RTCMMessage object :rtype: RTCMMessage :raises: RTCMParseError (if data stream contains invalid data or unknown message type) diff --git a/src/pyrtcm/rtcmtypes_core.py b/src/pyrtcm/rtcmtypes_core.py index 74efb5e..29c0187 100644 --- a/src/pyrtcm/rtcmtypes_core.py +++ b/src/pyrtcm/rtcmtypes_core.py @@ -155,8 +155,8 @@ INTS32 = "SNT032" # 32 bit sign-magnitude integer UTF8 = "UTF008" # Unicode UTF-8 Code Unit PRN = "PRN000" # Derived satellite PRN -CEL = "CEL000" # Derived cell (prn/signal) - +CELPRN = "CPR000" # Derived cell PRN +CELSIG = "CSG000" # Derived cell Signal ID # **************************************************** # THESE ARE THE RTCM PROTOCOL DATA FIELDS @@ -170,7 +170,8 @@ # **************************************************** RTCM_DATA_FIELDS = { "PRN": (PRN, 0, "Derived satellite PRN"), - "CELL": (CEL, 0, "Derived satellite PRN & signal ID"), + "CELLPRN": (CELPRN, 0, "Derived satellite PRN"), + "CELLSIG": (CELSIG, 0, "Derived satellite Signal ID"), "DF001": (BIT1, 0, "Reserved Field"), "DF001_1": (BIT1, 0, "Reserved 1 bit"), "DF001_2": (BIT2, 0, "Reserved 2 bits"), @@ -930,21 +931,6 @@ # "4095": "Ashtech", } -# list of MSM attributes to label if `labelmsm` is True -ATT_NSAT = ["DF397", "DF398", "DF399", "DF419", "ExtSatInfo"] -ATT_NCELL = [ - "DF400", - "DF401", - "DF402", - "DF403", - "DF404", - "DF405", - "DF406", - "DF407", - "DF408", - "DF420", -] - # map of MSM msg identity to GNSS name, epoch attribute name GNSSMAP = { "107": ("GPS", "DF004"), diff --git a/src/pyrtcm/rtcmtypes_get.py b/src/pyrtcm/rtcmtypes_get.py index 1500df7..155d9fb 100644 --- a/src/pyrtcm/rtcmtypes_get.py +++ b/src/pyrtcm/rtcmtypes_get.py @@ -183,7 +183,8 @@ "groupsig0": ( NCELL, { - "CELL": "GNSS Satellite PRN & Signal ID", + "CELLPRN": "GNSS Satellite PRN ", + "CELLSIG": "GNSS Satellite Signal ID", }, ), "groupsig1": ( @@ -198,7 +199,8 @@ "groupsig0": ( NCELL, { - "CELL": "GNSS Satellite PRN & Signal ID", + "CELLPRN": "GNSS Satellite PRN ", + "CELLSIG": "GNSS Satellite Signal ID", }, ), "groupsig1": ( @@ -226,7 +228,8 @@ "groupsig0": ( NCELL, { - "CELL": "GNSS Satellite PRN & Signal ID", + "CELLPRN": "GNSS Satellite PRN ", + "CELLSIG": "GNSS Satellite Signal ID", }, ), "groupsig1": ( @@ -259,7 +262,8 @@ "groupsig0": ( NCELL, { - "CELL": "GNSS Satellite PRN & Signal ID", + "CELLPRN": "GNSS Satellite PRN ", + "CELLSIG": "GNSS Satellite Signal ID", }, ), "groupsig1": ( @@ -298,7 +302,8 @@ "groupsig0": ( NCELL, { - "CELL": "GNSS Satellite PRN & Signal ID", + "CELLPRN": "GNSS Satellite PRN ", + "CELLSIG": "GNSS Satellite Signal ID", }, ), "groupsig1": ( @@ -343,7 +348,8 @@ "groupsig0": ( NCELL, { - "CELL": "GNSS Satellite PRN & Signal ID", + "CELLPRN": "GNSS Satellite PRN ", + "CELLSIG": "GNSS Satellite Signal ID", }, ), "groupsig1": ( @@ -382,7 +388,8 @@ "groupsig0": ( NCELL, { - "CELL": "GNSS Satellite PRN & Signal ID", + "CELLPRN": "GNSS Satellite PRN ", + "CELLSIG": "GNSS Satellite Signal ID", }, ), "groupsig1": ( diff --git a/tests/test_specialcases.py b/tests/test_specialcases.py deleted file mode 100644 index f807ad4..0000000 --- a/tests/test_specialcases.py +++ /dev/null @@ -1,207 +0,0 @@ -""" -Test special cases and methods - -Created on 7 Jul 2022 - -@author: semuadmin -""" -# pylint: disable=line-too-long, invalid-name, missing-docstring, no-member - -import os -import unittest -from collections import namedtuple - -from pyrtcm import RTCMMessage, RTCMReader, RTCMTypeError -from pyrtcm.rtcmhelpers import cell2prn, sat2prn - - -class SpecialTest(unittest.TestCase): - def setUp(self): - self.maxDiff = None - dirname = os.path.dirname(__file__) - self.streamMSM3 = open(os.path.join(dirname, "pygpsdata-RTCMMSM3.log"), "rb") - self.streamRTCM3 = open(os.path.join(dirname, "pygpsdata-RTCM3.log"), "rb") - - def tearDown(self): - self.streamMSM3.close() - self.streamRTCM3.close() - - def testsat2prn_synthetic( - self, - ): # test sat2prn helper method with synthetic messages - MSM = namedtuple("MSM", ["identity", "DF394", "DF395", "DF396"]) - EXPECTED_RESULTS_SAT = [ - {1: "001", 2: "003", 3: "007", 4: "063"}, - {1: "002", 2: "003", 3: "007", 4: "063"}, - {1: "003", 2: "007", 3: "063", 4: "Reserved"}, - {1: "193", 2: "195", 3: "196", 4: "199"}, - {1: "120", 2: "121", 3: "122", 4: "123", 5: "126"}, - ] - EXPECTED_RESULTS_CELL = [ - { - 1: ("001", "1P"), - 2: ("003", "1C"), - 3: ("007", "1C"), - 4: ("007", "1P"), - 5: ("063", "1C"), - 6: ("063", "1P"), - }, - {1: ("002", "1W"), 2: ("003", "1P"), 3: ("003", "1W")}, - {1: ("007", "2X"), 2: ("063", "2X"), 3: ("Reserved", "2X")}, - { - 1: ("193", "1C"), - 2: ("193", "1S"), - 3: ("193", "1L"), - 4: ("193", "1X"), - 5: ("195", "1C"), - 6: ("195", "1S"), - 7: ("195", "1L"), - 8: ("195", "1X"), - 9: ("196", "1C"), - 10: ("196", "1S"), - 11: ("196", "1L"), - 12: ("196", "1X"), - 13: ("199", "1C"), - 14: ("199", "1S"), - 15: ("199", "1L"), - 16: ("199", "1X"), - }, - { - 1: ("120", "5I"), - 2: ("120", "5Q"), - 3: ("120", "5X"), - 4: ("121", "5X"), - 5: ("122", "5I"), - 6: ("122", "5X"), - 7: ("123", "1C"), - 8: ("123", "5I"), - 9: ("126", "1C"), - 10: ("126", "5Q"), - }, - ] - msgs = [ - MSM( - "1077", - 0b1010001000000000000000000000000000000000000000000000000000000010, - 0b01100000000000000000000000000000, - 0b01101111, - ), - MSM( - "1077", - 0b0110001000000000000000000000000000000000000000000000000000000010, - 0b00110000000000000000000000000000, - 0b01110000, - ), - MSM( - "1127", - 0b0010001000000000000000000000000000000000000000000000000000000011, - 0b00010000000000000000000000000000, - 0b0111, - ), - MSM( - "1117", - 0b1011001000000000000000000000000000000000000000000000000000000000, - 0b01000000000000000000000000000111, - 0b1111111111111111, - ), - MSM( - "1107", - 0b1111001000000000000000000000000000000000000000000000000000000000, - 0b01000000000000000000011100000000, - 0b01110001010111001010, - ), - ] - for i, msg in enumerate(msgs): - res = sat2prn(msg) - self.assertEqual(res, EXPECTED_RESULTS_SAT[i]) - for i, msg in enumerate(msgs): - res = cell2prn(msg) - # print(res) - self.assertEqual(res, EXPECTED_RESULTS_CELL[i]) - - def testsat2prn(self): # test sat2prn helper method - EXPECTED_RESULT = [ - "{1: '006', 2: '011', 3: '012', 4: '017', 5: '019', 6: '020', 7: '024', 8: '025'}", - "{1: '002', 2: '009', 3: '015', 4: '016', 5: '017', 6: '018', 7: '019'}", - "{1: '002', 2: '010', 3: '011', 4: '012', 5: '024', 6: '025', 7: '036'}", - ] - - rtr = RTCMReader(self.streamMSM3) - idx = 0 - for raw, parsed in rtr: - if raw is not None: - if parsed.identity in ["1073", "1083", "1093", "1103", "1123"]: - res = str(sat2prn(parsed)) - # print(f'"{res}",') - self.assertEqual(res, EXPECTED_RESULT[idx]) - idx += 1 - - def testsat2prnerr(self): # test sat2prn helper method with invalid message - EXPECTED_ERROR = "Invalid RTCM3 message type - must be MSM message." - with self.assertRaisesRegex(RTCMTypeError, EXPECTED_ERROR): - rtr = RTCMReader(self.streamRTCM3) - for raw, parsed in rtr: - if raw is not None: - if parsed.identity in ["1230"]: - res = str(sat2prn(parsed)) - - def testcell2prn(self): # test cell2prn helper method using signal RINEX code - EXPECTED_RESULT = [ - "{1: ('006', '1C'), 2: ('006', '2X'), 3: ('006', '5X'), 4: ('011', '1C'), 5: ('011', '2X'), 6: ('011', '5X'), 7: ('012', '1C'), 8: ('012', '2X'), 9: ('017', '1C'), 10: ('017', '2X'), 11: ('019', '1C'), 12: ('019', '2W'), 13: ('020', '1C'), 14: ('020', '2W'), 15: ('024', '1C'), 16: ('024', '2X'), 17: ('024', '5X'), 18: ('025', '1C'), 19: ('025', '2X'), 20: ('025', '5X')}", - "{1: ('002', '1C'), 2: ('002', '2C'), 3: ('009', '1C'), 4: ('009', '2C'), 5: ('015', '1C'), 6: ('015', '2C'), 7: ('016', '1C'), 8: ('016', '2C'), 9: ('017', '1C'), 10: ('017', '2C'), 11: ('018', '1C'), 12: ('018', '2C'), 13: ('019', '1C'), 14: ('019', '2C')}", - "{1: ('002', '1X'), 2: ('002', '6X'), 3: ('002', '8X'), 4: ('010', '1X'), 5: ('010', '6X'), 6: ('010', '8X'), 7: ('011', '1X'), 8: ('011', '6X'), 9: ('011', '8X'), 10: ('012', '1X'), 11: ('012', '6X'), 12: ('012', '8X'), 13: ('024', '1X'), 14: ('024', '6X'), 15: ('024', '8X'), 16: ('025', '1X'), 17: ('025', '6X'), 18: ('025', '8X'), 19: ('036', '1X'), 20: ('036', '6X'), 21: ('036', '8X')}", - ] - - rtr = RTCMReader(self.streamMSM3) - idx = 0 - for raw, parsed in rtr: - if raw is not None: - if parsed.identity in ["1073", "1083", "1093", "1103", "1123"]: - res = str(cell2prn(parsed)) - # print(f'"{res}",') - self.assertEqual(res, EXPECTED_RESULT[idx]) - idx += 1 - - def testcell2prn2(self): # test cell2prn helper method using frequency band - EXPECTED_RESULT = [ - "{1: ('006', 'L1'), 2: ('006', 'L2'), 3: ('006', 'L5'), 4: ('011', 'L1'), 5: ('011', 'L2'), 6: ('011', 'L5'), 7: ('012', 'L1'), 8: ('012', 'L2'), 9: ('017', 'L1'), 10: ('017', 'L2'), 11: ('019', 'L1'), 12: ('019', 'L2'), 13: ('020', 'L1'), 14: ('020', 'L2'), 15: ('024', 'L1'), 16: ('024', 'L2'), 17: ('024', 'L5'), 18: ('025', 'L1'), 19: ('025', 'L2'), 20: ('025', 'L5')}", - "{1: ('002', 'G1'), 2: ('002', 'G2'), 3: ('009', 'G1'), 4: ('009', 'G2'), 5: ('015', 'G1'), 6: ('015', 'G2'), 7: ('016', 'G1'), 8: ('016', 'G2'), 9: ('017', 'G1'), 10: ('017', 'G2'), 11: ('018', 'G1'), 12: ('018', 'G2'), 13: ('019', 'G1'), 14: ('019', 'G2')}", - "{1: ('002', 'E1'), 2: ('002', 'E6'), 3: ('002', 'E5AB'), 4: ('010', 'E1'), 5: ('010', 'E6'), 6: ('010', 'E5AB'), 7: ('011', 'E1'), 8: ('011', 'E6'), 9: ('011', 'E5AB'), 10: ('012', 'E1'), 11: ('012', 'E6'), 12: ('012', 'E5AB'), 13: ('024', 'E1'), 14: ('024', 'E6'), 15: ('024', 'E5AB'), 16: ('025', 'E1'), 17: ('025', 'E6'), 18: ('025', 'E5AB'), 19: ('036', 'E1'), 20: ('036', 'E6'), 21: ('036', 'E5AB')}", - ] - - rtr = RTCMReader(self.streamMSM3) - idx = 0 - for raw, parsed in rtr: - if raw is not None: - if parsed.identity in ["1073", "1083", "1093", "1103", "1123"]: - res = str(cell2prn(parsed, 0)) - # print(f'"{res}",') - self.assertEqual(res, EXPECTED_RESULT[idx]) - idx += 1 - - def testcell2prnerr(self): # test cell2prn helper method with invalid message - EXPECTED_ERROR = "Invalid RTCM3 message type - must be MSM message." - rtr = RTCMReader(self.streamRTCM3) - with self.assertRaisesRegex(RTCMTypeError, EXPECTED_ERROR): - for raw, parsed in rtr: - if raw is not None: - if parsed.identity in ["1230"]: - res = str(cell2prn(parsed)) - - def testunknown(self): # test (synthetic) unknown messages - EXPECTED_RESULTS = [ - "", - "", - ] - PAYLOADS = [ - b"\xfd\xe1\x81\xc9\x84\x00\x08\xc2\xb8\x88\x00\x38\x80\x09\xd0\x46\x00\x28", - b"\x3e\x71\x81\xc9\x84\x00\x08\xc2\xb8\x88\x00\x38\x80\x09\xd0\x46\x00\x28", - ] - for i, pay in enumerate(PAYLOADS): - msg = RTCMMessage(payload=pay) - self.assertEqual(str(msg), EXPECTED_RESULTS[i]) - - -if __name__ == "__main__": - # import sys;sys.argv = ['', 'Test.testName'] - unittest.main() diff --git a/tests/test_static.py b/tests/test_static.py index da6b647..7925281 100644 --- a/tests/test_static.py +++ b/tests/test_static.py @@ -283,8 +283,8 @@ def testparsemsm(self): ], [ { - "PRN": "005", - "SIGNAL": "1C", + "CELLPRN": "005", + "CELLSIG": "1C", "DF404": -0.9231, "DF405": 0.00014309026300907135, "DF406": 0.00014193402603268623, @@ -293,8 +293,8 @@ def testparsemsm(self): "DF420": 0, }, { - "PRN": "005", - "SIGNAL": "2L", + "CELLPRN": "005", + "CELLSIG": "2L", "DF404": -0.9194, "DF405": 0.00014183297753334045, "DF406": 0.00014339853078126907, @@ -303,8 +303,8 @@ def testparsemsm(self): "DF420": 0, }, { - "PRN": "007", - "SIGNAL": "1C", + "CELLPRN": "007", + "CELLSIG": "1C", "DF404": -0.8321000000000001, "DF405": 0.0003883279860019684, "DF406": 0.00039040297269821167, @@ -313,8 +313,8 @@ def testparsemsm(self): "DF420": 0, }, { - "PRN": "007", - "SIGNAL": "2L", + "CELLPRN": "007", + "CELLSIG": "2L", "DF404": -0.8326, "DF405": 0.00038741156458854675, "DF406": 0.00038743019104003906, @@ -323,8 +323,8 @@ def testparsemsm(self): "DF420": 0, }, { - "PRN": "009", - "SIGNAL": "1C", + "CELLPRN": "009", + "CELLSIG": "1C", "DF404": -0.4107, "DF405": -0.0004838351160287857, "DF406": -0.0004843934439122677, @@ -333,8 +333,8 @@ def testparsemsm(self): "DF420": 0, }, { - "PRN": "009", - "SIGNAL": "2L", + "CELLPRN": "009", + "CELLSIG": "2L", "DF404": -0.4072, "DF405": -0.00046883709728717804, "DF406": -0.00046825408935546875, @@ -343,8 +343,8 @@ def testparsemsm(self): "DF420": 0, }, { - "PRN": "013", - "SIGNAL": "1C", + "CELLPRN": "013", + "CELLSIG": "1C", "DF404": 0.2451, "DF405": 0.0003478657454252243, "DF406": 0.0003473707474768162, @@ -353,8 +353,8 @@ def testparsemsm(self): "DF420": 0, }, { - "PRN": "014", - "SIGNAL": "1C", + "CELLPRN": "014", + "CELLSIG": "1C", "DF404": -0.0693, "DF405": 0.0002196934074163437, "DF406": 0.00021758908405900002, @@ -363,8 +363,8 @@ def testparsemsm(self): "DF420": 0, }, { - "PRN": "014", - "SIGNAL": "2L", + "CELLPRN": "014", + "CELLSIG": "2L", "DF404": -0.0684, "DF405": 0.00021521002054214478, "DF406": 0.00021597417071461678, @@ -373,8 +373,8 @@ def testparsemsm(self): "DF420": 0, }, { - "PRN": "015", - "SIGNAL": "1C", + "CELLPRN": "015", + "CELLSIG": "1C", "DF404": 0.9390000000000001, "DF405": -0.00018852390348911285, "DF406": -0.00018658116459846497, @@ -383,8 +383,8 @@ def testparsemsm(self): "DF420": 0, }, { - "PRN": "015", - "SIGNAL": "2L", + "CELLPRN": "015", + "CELLSIG": "2L", "DF404": 0.9417000000000001, "DF405": -0.00018319115042686462, "DF406": -0.00018350128084421158, @@ -393,8 +393,8 @@ def testparsemsm(self): "DF420": 0, }, { - "PRN": "017", - "SIGNAL": "1C", + "CELLPRN": "017", + "CELLSIG": "1C", "DF404": 0.2384, "DF405": -0.00010087713599205017, "DF406": -9.993184357881546e-05, @@ -403,8 +403,8 @@ def testparsemsm(self): "DF420": 0, }, { - "PRN": "017", - "SIGNAL": "2L", + "CELLPRN": "017", + "CELLSIG": "2L", "DF404": 0.2416, "DF405": -9.844452142715454e-05, "DF406": -9.724870324134827e-05, @@ -413,8 +413,8 @@ def testparsemsm(self): "DF420": 0, }, { - "PRN": "019", - "SIGNAL": "1C", + "CELLPRN": "019", + "CELLSIG": "1C", "DF404": 0.6636000000000001, "DF405": 0.00047875382006168365, "DF406": 0.0004128236323595047, @@ -423,8 +423,8 @@ def testparsemsm(self): "DF420": 0, }, { - "PRN": "020", - "SIGNAL": "1C", + "CELLPRN": "020", + "CELLSIG": "1C", "DF404": -0.9556, "DF405": 0.00043664872646331787, "DF406": 0.0004355977289378643, @@ -433,8 +433,8 @@ def testparsemsm(self): "DF420": 0, }, { - "PRN": "030", - "SIGNAL": "1C", + "CELLPRN": "030", + "CELLSIG": "1C", "DF404": -0.21480000000000002, "DF405": -0.0003105681389570236, "DF406": -0.0003112703561782837, @@ -443,8 +443,8 @@ def testparsemsm(self): "DF420": 0, }, { - "PRN": "030", - "SIGNAL": "2L", + "CELLPRN": "030", + "CELLSIG": "2L", "DF404": -0.2174, "DF405": -0.00030865520238876343, "DF406": -0.00030898721888661385, diff --git a/tests/test_stream.py b/tests/test_stream.py index 99a4650..55225bc 100644 --- a/tests/test_stream.py +++ b/tests/test_stream.py @@ -103,9 +103,9 @@ def testMSM3( dirname = os.path.dirname(__file__) with open(os.path.join(dirname, "pygpsdata-RTCMMSM3.log"), "rb") as stream: EXPECTED_RESULT = ( - "", - "", - "", + "", + "", + "", ) rtr = RTCMReader(stream, quitonerror=2) i = 0 @@ -121,20 +121,20 @@ def testMIXEDRTCM_NOSCALE( EXPECTED_RESULTS = ( "", "", - "", - "", - "", - "", + "", + "", + "", + "", "", "", - "", + "", ) dirname = os.path.dirname(__file__) with open(os.path.join(dirname, "pygpsdata-RTCM3.log"), "rb") as stream: i = 0 raw = 0 - rtr = RTCMReader(stream, scaling=False, labelmsm=False) + rtr = RTCMReader(stream, scaling=False) for raw, parsed in rtr: if raw is not None: # print(f'"{parsed}",') @@ -148,19 +148,19 @@ def testMIXEDRTCM_SCALE( EXPECTED_RESULTS = ( "", "", - "", - "", - "", - "", + "", + "", + "", + "", "", "", - "", + "", ) dirname = os.path.dirname(__file__) with open(os.path.join(dirname, "pygpsdata-RTCM3.log"), "rb") as stream: i = 0 raw = 0 - rtr = RTCMReader(stream, scaling=True, labelmsm=False) + rtr = RTCMReader(stream, scaling=True) for raw, parsed in rtr: if raw is not None: # print(f'"{parsed}",') @@ -174,13 +174,13 @@ def testMIXEDRTCM_SCALE_LABELMSM_RINEX( EXPECTED_RESULTS = ( "", "", - "", - "", - "", - "", + "", + "", + "", + "", "", "", - "", + "", ) dirname = os.path.dirname(__file__) @@ -203,13 +203,13 @@ def testMIXEDRTCM_SCALE_LABELMSM_FREQ( EXPECTED_RESULTS = ( "", "", - "", - "", - "", - "", + "", + "", + "", + "", "", "", - "", + "", ) dirname = os.path.dirname(__file__) @@ -248,18 +248,18 @@ def testntrip2log( "", "", "", - "", - "", - "", - "", - "", - "", - "", - "", + "", + "", + "", + "", + "", + "", + "", + "", "", "", - "", - "", + "", + "", "", "", "", From fc5c906497f8353494f6e1568622862f2b846a20 Mon Sep 17 00:00:00 2001 From: semuadmin <28569967+semuadmin@users.noreply.github.com> Date: Sun, 12 May 2024 17:12:44 +0100 Subject: [PATCH 3/7] update version to 1.1.0 --- .vscode/settings.json | 2 +- RELEASE_NOTES.md | 4 ++++ pyproject.toml | 2 +- src/pyrtcm/_version.py | 2 +- 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 69b42c2..2c370a5 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,5 +4,5 @@ "editor.formatOnSave": true, "modulename": "${workspaceFolderBasename}", "distname": "${workspaceFolderBasename}", - "moduleversion": "1.0.20" + "moduleversion": "1.1.0" } \ No newline at end of file diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 1bb1461..f4868c8 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,5 +1,9 @@ # pyrtcm Release Notes +### RELEASE 1.1.0 + +ENHANCEMENTS: + ### RELEASE 1.0.20 ENHANCEMENTS diff --git a/pyproject.toml b/pyproject.toml index 35e699a..673b74e 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.20" +version = "1.1.0" license = { file = "LICENSE" } readme = "README.md" requires-python = ">=3.8" diff --git a/src/pyrtcm/_version.py b/src/pyrtcm/_version.py index 6621249..b342a03 100644 --- a/src/pyrtcm/_version.py +++ b/src/pyrtcm/_version.py @@ -8,4 +8,4 @@ :license: BSD 3-Clause """ -__version__ = "1.0.20" +__version__ = "1.1.0" From 0373891e42929bcf66129f8488a52f068e2a125f Mon Sep 17 00:00:00 2001 From: semuadmin <28569967+semuadmin@users.noreply.github.com> Date: Sun, 12 May 2024 21:06:09 +0100 Subject: [PATCH 4/7] update readme & release notes --- README.md | 22 +++++++++++----------- RELEASE_NOTES.md | 2 ++ src/pyrtcm/rtcmtypes_get.py | 36 ++++++++++++++++++------------------ 3 files changed, 31 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index c60df8a..6083b3a 100644 --- a/README.md +++ b/README.md @@ -105,7 +105,7 @@ with Serial('/dev/tty.usbmodem14101', 9600, timeout=3) as stream: print(parsed_data) ``` ``` - , +"", ``` Example - File input (using iterator). @@ -145,19 +145,19 @@ print(msg) ``` -The `RTCMMessage` object exposes different public attributes depending on its message type or 'identity'. Attributes are defined as data fields (`DF002`, `DF003`, etc.) e.g. the `1087` multiple signal message (MSM) contains the following data fields: +The `RTCMMessage` object exposes different public attributes depending on its message type or 'identity'. Attributes are defined as data fields (`DF002`, `DF003`, etc.) e.g. the `1097` multiple signal message (MSM) contains the following data fields: ```python print(msg) print(msg.identity) -print(msg.DF034) -print(msg.DF419_03) +print(msg.DF248) +print(msg.DF404_07) ``` ``` - -'1087' -42119001 -8 +"", +'1097' +204137001 +5534 ``` Attributes within repeating groups are parsed with a two-digit suffix (`DF419_01`, `DF419_02`, etc. See [example below](#iterating) for an illustration of how to iterate through grouped attributes). @@ -183,20 +183,20 @@ The `payload` attribute always contains the raw payload as bytes. #### Iterating Through Group Attributes -To iterate through a group of one or more repeating attributes in a given `RTCMMessage` object, the following construct can be used (in this illustration, repeating attributes DF405, DF406, DF407, DF408, DF420 and DF404 are extracted from an MSM 1077 message `msg` and collated in the array `msmarray`): +To iterate through a group of one or more repeating attributes in a given `RTCMMessage` object, the following construct can be used (in this illustration, repeating attributes CELLPRN, CELLSIG, DF405, DF406, DF407, DF408, DF420 and DF404 are extracted from an MSM 1077 message `msg` and collated in the array `msmarray`): ```python msmarray = [] for i in range(msg.NCell): # msg = MSM 1077, number of cells = NCell vals = [] - for attr in ("DF405", "DF406", "DF407", "DF408", "DF420", "DF404"): + for attr in ("CELLPRN", "CELLSIG", "DF405", "DF406", "DF407", "DF408", "DF420", "DF404"): val = getattr(msg, f"{attr}_{i+1:02d}") vals.append(val) msmarray.append(vals) print(msmarray) ``` ```shell -[[0.00014309026300907135, 0.00014193402603268623, 341, 45.0, 0, -0.9231], [0.00014183297753334045, 0.00014339853078126907, 341, 38.0, 0, -0.9194], ... etc.] +[['005', '1C', 0.00014309026300907135, 0.00014193402603268623, 341, 45.0, 0, -0.9231], ..., ['030', '2L', -0.00030865520238876343, -0.00030898721888661385, 341, 41.0, 0, -0.2174]] ``` The following dedicated helper methods are available to parse selected RTCM3 message types into a series of iterable data arrays: diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index f4868c8..0586052 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -4,6 +4,8 @@ ENHANCEMENTS: +1. `PRN`, `CELLPRN` and `CELLSIG` attributes added to satellite (NSAT) and cell (NCELL) groups within parsed RTCM3 MSM payloads via `SPARTNMessage._getsatcellmaps()` function, replacing previous `sat2prn()` and `cell2prn()` helper functionality. `labelmsm` keyword argument signifies either RINEX (1) or Frequency Band (2) signal format for CELLSIG attribute. + ### RELEASE 1.0.20 ENHANCEMENTS diff --git a/src/pyrtcm/rtcmtypes_get.py b/src/pyrtcm/rtcmtypes_get.py index 155d9fb..6b8bc7b 100644 --- a/src/pyrtcm/rtcmtypes_get.py +++ b/src/pyrtcm/rtcmtypes_get.py @@ -80,7 +80,7 @@ "groupsat0": ( NSAT, { - "PRN": "GNSS Satellite PRN", + "PRN": "GNSS Satellite PRN", # derived by pyspartn }, ), "groupsat1": ( @@ -95,7 +95,7 @@ "groupsat0": ( NSAT, { - "PRN": "GNSS Satellite PRN", + "PRN": "GNSS Satellite PRN", # derived by pyspartn }, ), "groupsat1": ( @@ -117,7 +117,7 @@ "groupsat0": ( NSAT, { - "PRN": "GNSS Satellite PRN", + "PRN": "GNSS Satellite PRN", # derived by pyspartn }, ), "groupsat1": ( @@ -150,7 +150,7 @@ "groupsat0": ( NSAT, { - "PRN": "GNSS Satellite PRN", + "PRN": "GNSS Satellite PRN", # derived by pyspartn }, ), "groupsat1": ( @@ -183,8 +183,8 @@ "groupsig0": ( NCELL, { - "CELLPRN": "GNSS Satellite PRN ", - "CELLSIG": "GNSS Satellite Signal ID", + "CELLPRN": "GNSS Satellite PRN ", # derived by pyspartn + "CELLSIG": "GNSS Satellite Signal ID", # derived by pyspartn }, ), "groupsig1": ( @@ -199,8 +199,8 @@ "groupsig0": ( NCELL, { - "CELLPRN": "GNSS Satellite PRN ", - "CELLSIG": "GNSS Satellite Signal ID", + "CELLPRN": "GNSS Satellite PRN ", # derived by pyspartn + "CELLSIG": "GNSS Satellite Signal ID", # derived by pyspartn }, ), "groupsig1": ( @@ -228,8 +228,8 @@ "groupsig0": ( NCELL, { - "CELLPRN": "GNSS Satellite PRN ", - "CELLSIG": "GNSS Satellite Signal ID", + "CELLPRN": "GNSS Satellite PRN ", # derived by pyspartn + "CELLSIG": "GNSS Satellite Signal ID", # derived by pyspartn }, ), "groupsig1": ( @@ -262,8 +262,8 @@ "groupsig0": ( NCELL, { - "CELLPRN": "GNSS Satellite PRN ", - "CELLSIG": "GNSS Satellite Signal ID", + "CELLPRN": "GNSS Satellite PRN ", # derived by pyspartn + "CELLSIG": "GNSS Satellite Signal ID", # derived by pyspartn }, ), "groupsig1": ( @@ -302,8 +302,8 @@ "groupsig0": ( NCELL, { - "CELLPRN": "GNSS Satellite PRN ", - "CELLSIG": "GNSS Satellite Signal ID", + "CELLPRN": "GNSS Satellite PRN ", # derived by pyspartn + "CELLSIG": "GNSS Satellite Signal ID", # derived by pyspartn }, ), "groupsig1": ( @@ -348,8 +348,8 @@ "groupsig0": ( NCELL, { - "CELLPRN": "GNSS Satellite PRN ", - "CELLSIG": "GNSS Satellite Signal ID", + "CELLPRN": "GNSS Satellite PRN ", # derived by pyspartn + "CELLSIG": "GNSS Satellite Signal ID", # derived by pyspartn }, ), "groupsig1": ( @@ -388,8 +388,8 @@ "groupsig0": ( NCELL, { - "CELLPRN": "GNSS Satellite PRN ", - "CELLSIG": "GNSS Satellite Signal ID", + "CELLPRN": "GNSS Satellite PRN ", # derived by pyspartn + "CELLSIG": "GNSS Satellite Signal ID", # derived by pyspartn }, ), "groupsig1": ( From b99807d7a93f876581e56a64ae69390caf838236 Mon Sep 17 00:00:00 2001 From: semuadmin <28569967+semuadmin@users.noreply.github.com> Date: Mon, 13 May 2024 04:26:26 +0100 Subject: [PATCH 5/7] streamline _getsatcellmaps --- examples/benchmark.py | 46 ++++++++++++++++++++++++++++-------- src/pyrtcm/rtcmmessage.py | 15 +++++++----- src/pyrtcm/rtcmtypes_core.py | 1 + 3 files changed, 46 insertions(+), 16 deletions(-) diff --git a/examples/benchmark.py b/examples/benchmark.py index 0c91ecc..41e0c73 100644 --- a/examples/benchmark.py +++ b/examples/benchmark.py @@ -9,6 +9,7 @@ :copyright: SEMU Consulting © 2022 :license: BSD 3-Clause """ + # pylint: disable=line-too-long from platform import python_version @@ -20,16 +21,41 @@ from pyrtcm.rtcmreader import RTCMReader RTCMMESSAGES = [ - b"\xd3\x00\x13>\xd0\x00\x03\x8aX\xd9I<\x87/4\x10\x9d\x07\xd6\xafH Z\xd7\xf7", - b"\xd3\x00\x08>\xf4\xd2\x03ABC\xeapo\xc7", - b"\xd3\x00\x12B\x91\x81\xc9\x84\x00\x04B\xb8\x88\x008\x80\t\xd0F\x00(\xf0kf", - b"\xd3\x00\x13>\xd0\x00\x03\x8aX\xd9I<\x87/4\x10\x9d\x07\xd6\xafH Z\xd7\xf7", - b"\xd3\x00>\xfe\x80\x01\x00\x00\x00\x13\n\xb8\x8a@\x00\x00\x08\x00\x00\x00\x00\x00\x00\x00\x01\xff\x9f\x00\x16\x02\x00\xfe\\\x00\x19\x02\x01\xfe\xdd\x00\x1d\x03\x00\x02\x86\x00\x13\x05\x00\x00\x00\x01\x90\x06\x00\x03\xf7\x00\x1a\x06\x01\x04%\x00\x1e\xd2O,", - b"\xd3\x01\rCP\x000\xab\x88\xa6\x00\x00\x05GX\x02\x00\x00\x00\x00 \x00\x80\x00\x7f\x7fZZZ\x8aB\x1a\x82Z\x92Z8\x00\x00\x00\x00\x00\r\x11\xe1\xa4tf:f\xe3L,\xb1~\x9d\xf6\x87\xaf\xa0\xee\xff\x98\x14(B!A\xfc\xa9\xfaX\x96\n\x89K\x91\x971\x19c\xb6\x04\xa9\xe1F9l\xc3\x8ee\xd8\xe1\xaas\xa5\x1f?\xe9yc\x97\x98\xc6\x1f`)\xc9\xdck\xa5\x8e\xbcZ\x02SP\x82Yu\x06ex\x06Y\x00x\x10N\xf8T\x00\x05\xb0\xfa\x83\x90\xa2\x83\x89\xdc\xfc\xf1l|\xfeW~\\\xdb~h\x1c\x06\xc3\x82\x07#\x07\xfa\xe6pz\xf0\x03\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xa9:\xaa\xaa\xaa\xa0\x00\x0bB`\xac'\t\xc2P\xb4.\x0b\x82p\x88-\t\x81\xf0\xb4.\nB\xdf\x8d\xc1k\xef\xf7\xde\xb7\xfa\xf0\x18\x13'\xf5/\xea\xa2J\xe4\x99\"T\x04\xb8\x19\xec\xb5Y\xdes\xbc\x10\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xa9t\xd0", - b"\xd3\x00\xc3C\xf0\x00J\n\xbdf\x00\x00\x1c\x07\x01\x00\x00\x00\x00\x00 \x80\x00\x00\x7f\xfc\x8a\x80\x92\x98\x84\x8c\x9d\x9b\n\x0fTJ\xbe\x82'\xd0n\x9f\xc4\xfa\xce\x00\xe8T\x1e\xe1\xfeZ\t\xc0'\xa4\x15\xe6A\xd7_;\xc1\xf2\x85`.\xbe\x05\xa3'\xb6\xa6}\xb2y\xa4\xf5\x9dl\x84\x8a\x98KE\xfc!\xa6\x10W\xc8\x10oM\xfc\xd4\xe9\xfc\xa4<\x00\xbb\x0e\x01m\xcc\x1e\xd1\xb6\x1f\xc6\x0f\xe6\x98\xf1\xe7_4\x126\x18\x12\xe1\x05\xf0x\x14\xaa\xaa\xaa\xa2\xa8\xaa\xaa\xaa\xa2\xaa\xaa\xaa\xaa\xaa\xaa\xaa\x00\x02\xf0\xa0/\n\x82\xf0\x9c$\x08C\x00\xac0\n\x02\x90\xbf\xff\x80M\n\xda\x13S\x94\xa7#\xfb!\xf6\x11\xef%\xdd\xf8z\xa0\xf6\xb3\x00@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00v'\xaf", - b"\xd3\x00\x91D\x90\x000\xab\x88\xa6\x00\x00\x01\x80\x04\x12\x00\x00\x00\x00 \x01\x00\x00\x7f\xe9\xea\x8b)\xca`\x00\x00P +Z\xf8\x85~u\xef\xe04\xe0\x1f\xfd\x01\xf4\x19\x7f\x89\x81\xa5N:\xa52~\x15h6e\xdc\x18\xdd\xefY\xfb*\x9f\xf3?\xfd\x16Q\xfe$K\xe8\xe5;\xea\x9c\\\x1f\x97D \xd2\xc9\xf6\xfb\xf5\xf7\xb8\x19\xfe\xd1a\xff\xc8\xc2\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xa0\x05\xc1\x88R\x15\x85aXZ\x18\x85axiR\xd2s\x83\xd7\x07\xc9\xc4\xb3\x80\xc5g\x8a\xd4\xe1\xd2\xc3\x96\x00\x08y", - b'\xd3\x01\rFp\x000\xaa\xad\xe4\x00\x00\x01`\t\x08\x84\x90\x00\x00 \x02\x00\x00/UT\x0c#\xf2Z\x8a\xa2rT\x12\xb0\x00\x00\x00\x00\x00\xf0\xf6\xa7\xb7;I$G\xaaT\xa1Y~\xfd\xfe7\xf5\xe0\x10|\xe4\r\xa7\xbe\xbf\xdf\xfe\x94\x02~h\x96\x0e\xe5\x89\xa7E\x19\xf4\xf7Q\x0e|\xe29\x81Q\x91s\xc6\xf9\x95\xf8C\xae\xcb\xf6\xf9\xa3\xbd\x83\xb5\xfb\x06\x9b"\x86~\xb7}C\xca\x7f4\xa1\x06\x0e\xb2\x84Y6\xfb\xe2\x95~\x0e6{*\xdc\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\x00-\nB\xa0\xb40\x0b\x82\xa0\xbc0\x0b\x02\xb0\xd3\xad\xa0c\xd4\xc7\xac\xc2\xed\x18\xdc\x03bo,\xd1S\x96\xcc\xbfP\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xb2X\xbc', - b"\xd3\x00\x9dE\xd0\x00[\xfc\x95\x82\x00 1\x00\x00\x00\x00\x00\x00\x00 \x00@\x80\xff\xfcc\x84(\x00\x16\x04\x06\xfa\x0f\x0f\xfc\x80\x00\x8e\xd5q\x13\xcd\x10\x14\xe0\xf1t}z'\xf7D\xfe\x85\x17\xdd\x84y\x92\xdf\xb4\xe5z`\xf7\x9b\xa0\xfcf(\x084\xe1\xff\xefn\x83\xa3\x13\x86dJ}3\x15\xfdD\x03|\xea\xe0\x84\xa7\xe5\xfc\xcaS\x03^\xc9\x02\xe8\xa3@\x8euX\xb6\xd1\x14M\x13D\xc7\x16\x05\x81`\x00\x03\xcb\x1fE\x11F\x15\x94iY\x9c\xcb]\xdb\xb4/\xbd\x88\x00\x10\x00 \x00i\r\x80\x01\x00\x02\x00\x06\xfd\xe8\x00\x10\x00 \x00\x00\x92\x8e\xca", + b"\xd3\x00\x93>\xb0\x00L\n\xdb\xa2\xb0\t\xaec\xb1\xe1\xde\x7f\xf0hz\x9c{\xf8`T\x15\x9f\x99>\xff\x83\x17\xf3d_\xd5\x06\x93,s\xad\xc7\xfc0-\xe7k\xfe!\xb9\xbcd\x02\xe4\x7f\xe0\x8b\x01\xdb\xb7\xf2I\x17\xfa\xe0y#\xff\x06\x1c$E?\x8c\t\xff\x91\x01c\x0f\xf8G\xe0\xf7\x8f\xfd0\x851\xc7\xdd\x8c\xff\xc1h\xf9\x7f?\xef\x85w\x00\xfe\xe1\xe3\xfe\x10\xb7\xf1Y\xffDNL\xef\xef\xf7\xbf\xf0|\xbd\x88\xd7\xf8\xe7e\x19\x90\x02@e\x87,\x10.\xaaAB_\x8c}k7\xfc\x1ao\xba\xf5\xfe\xe5\x8c/", + b"\xd3\x00\xb4>\xc0\x00L\n\xdb\xa2\xb0\t\xaec\xb1\xe1\xde\x7f\xd2\xeb0hz\x9c{\xfb\xe8`T\x15\x9f\x99>\xfe\x8d\x8d\x83\x17\xf3d_\xf4\x15\x06\x93,s\xad\xc7\xf4\xf9\xac0-\xe7k\xfe\x9c!\xb9\xbcd\x02\xe4\x7f\xa1\xe2`\x8b\x01\xdb\xb7\xfa\xf2I\x17\xfa\xe0y#\xfd\x1f\x03\x06\x1c$E?\xe5\x0c\t\xff\x91\x01c\x0f\xe9\xd3\xf8G\xe0\xf7\x8f\xfe=0\x851\xc7\xdd\x8c\xffN\xa3\xc1h\xf9\x7f?\xed\xcf\x85w\x00\xfe\xe1\xe3\xfa}V\x10\xb7\xf1Y\xff\x94DNL\xef\xef\xf7\xbf\xd3(\xb0|\xbd\x88\xd7\xfcX\xe7e\x19\x90\x02@d\xa7\x11\x87,\x10.\xaaR\xc1B_\x8c}k7\xf4Ll\x1ao\xba\xf5\xff\xa0\xd4\x82+", + b"\xd3\x00\x13>\xd0\x00\x03\x84\x1a\x86\x92\xbf\xb4KK\xf4\xfa\xb7\xdc7b\x8a3\x84y", + b"\xd3\x00\x15>\xe0\x00\x03\x84\x1a\x86\x92\xbf\xb4KK\xf4\xfa\xb7\xdc7b\x8a\x01W\x1b\xa9\xd6", + b"\xd3\x00\x19>\xf0\x00\x14SEPCHOKE_B3E6 SPKE\x00\x07nf", + b"\xd3\x00\x1e?\x00\x00\x14SEPCHOKE_B3E6 SPKE\x00\x045856\xffh\x94", + b'\xd3\x00H?\x10\x00\x86\x85\x03\x14\x00$4\x07\xbaAt\x0b\xfa\xc2 3\x11=\xa0+\xfb\x04\xb8q\x80\xc0\xbd\xdf\xf9\x06\xb8\xd7U\x80u\xbb\xf8\xe6 \xc5\x18=#\xb7\xfa\xe5\\\x85\xd8\x80\\\x83\xf9@m\x8d%\x01\xb17\xf9"\xbdz\xc2\x81h\x03\xf8\x9f\xc6\xe6', + b'\xd3\x00W? \x00\x86\x85\x03\x14\x00$4\x07\xbaAt\x0b\xfaZe\x84@f"{@W\xf4x\xec\x12\xe1\xc6\x03\x02\xf7\x7f\xe9m\xc85\xc6\xba\xac\x03\xad\xdf\xd0\xe9\x8eb\x0cQ\x83\xd2;\x7f\xa4\xc1\\\xab\x90\xbb\x10\x0b\x90\x7fA\x96P\x1bcI@lM\xfe\x96p\x91^\xbda@\xb4\x01\xfd-\x90\xbb\x83T', + b"\xd3\x00s?0\x00\x86\x85\x03\x14\x00$4\x07\xbaAt\x0b\xf8\x17\x88KV\xe9XD\x06b'\xb4\x05\x7f\x02Y\xf7\xc6\x8f\xec\x12\xe1\xc6\x03\x02\xf7\x7f\xe0X\x01\xaf/\xfc\x83\\k\xaa\xc0:\xdd\xfc\x07h\x1f`\xbf\x8eb\x0cQ\x83\xd2;\x7f\x81\\z\x8fw\xf5\xca\xb9\x0b\xb1\x00\xb9\x07\xf2\x00\x08\x00\x00\x00P\x1bcI@lM\xfe@\x01\x00\x00\x00\t\x15\xeb\xd6\x14\x0b@\x1f\xc0\xe2\x859\x13\xf8'\xe0X", + b"\xd3\x00\x8a?@\x00\x86\x85\x03\x14\x00$4\x07\xbaAt\x0b\xfaZ`/\x10\x96\xad\xd3\x1c\xb0\x88\x0c\xc4Oh\n\xfe\x8f\x1c\tg\xdf\x1a?\xdc\xb0K\x87\x18\x0c\x0b\xdd\xff\xa5\xb7\x02\xc0\ry\x7f\xf5d\x1a\xe3]V\x01\xd6\xef\xe8t\xc0v\x81\xf6\x0b\xfe0\xe6 \xc5\x18=#\xb7\xfaL\x10+\x8fQ\xee\xfff\xb9W!v \x17 \xfe\x83,\x80\x02\x00\x00\x00\x00\x14\x06\xd8\xd2P\x1b\x13\x7f\xa5\x9c \x00\x80\x00\x00\x00\x04\x8a\xf5\xeb\n\x05\xa0\x0f\xe9l\x80\xe2\x859\x13\xfd8\x0e\x8f\xad", + b"\xd3\x00\t?P\x00\xeb\xdet\xa7\x80Hj\x9a\x85", + b"\xd3\x00=?\xb0\x90\x10z\xa4\xb9O\x1a\x00\x006\xc2HX\xb9\xf1W.\tX\x1c\x10S\xf3\xa6\x08@\xcey\x11\xf0\xa1\r\xb5\xfdO\x1a\x00\x82\x87\x11\xb6\xbb\x00\t'n\xc4\x03\x1aJ\xce1[\x86\xff\xaa\xe6\xda\x00\x8eV+", + b"\xd3\x00-?\xc2K\xb3x\xcf\xa0\xf4\x96L\xb5\xd1\xa0\r\x84\xba\x00!\x1b\xf2\xa7\xf6H\xbfY\x16c\x80-\x16\xf4\xa5\x01P\x92\xc2L\x00\x00\x00\x1a\x00\x00\x08\x00\xc2\xb9\xe5", + b"\xd3\x00\x10@P\x00\xeb\xdet\xa7\x87\x07Unknown\x95\x8eL", + b"\xd3\x009@\x90\x00\x14SEPCHOKE_B3E6 SPKE\x00\x045856\x0cSEPT POLARX5\x055.5.0\x073075024\xb9\x1f\xbd", + b"\xd3\x00@A#\x07j\x1d\xae\r5a\xfd\xbf\xdd\xca\xe40\x86\x17\xcc\x82M}\xe2\xf5^\x87\xea\xa4\x00H\x1c\xa7\x85\x19T\xa2\xa1\x07)\xab\x00\x01a\xd1\x8av\x03\xff\xd8(\x0b\xc4\xdd\x11!\xb1\r\x0f\xce\x7f\xec\xfc\x01\x80\x10\r8\x18", + b"\xd3\x00>AP\xd4\x04\x16k\xfb\xb9K@?\xfe\x89\xff\x97\x1c\x1b\xeb\xf0\xa0R\xea\xd2E\x93\xf0<\x00v5<#\xfa\xa8\x12\xf5\x11KO\xfe\xfe\n\x10\xff\x7f\xfe\xf2r8\xa5\xd1\xef\xdfR:5O\xfb\xf9\x904\x00\xe7/\xc4", + b'\xd3\x00?AaT\x04\x16k\xfb\xb1K@\x00\x01\xf4\x13^h\xc3\xe9\xe8\xa0\xbc#7\x08?\xef`\x00}\xaa\xcc"\xe6\xa8\x12\xf4\x19KO\xff\xde\n\x11\x82\xcf\xff\xd2r:\xe3\xe1\xf0\\l\xc0|\x0f\xfb\xf1\xe0L\x15\x00%A\xd4', + b'\xd3\x01\x89C@\x00L\n\xdb\xa2\x00 {@T\x00\x00\x00\x00\x00( \x81\x01}\xc7\xdf\xfd\xc7\xdeq\xc2"Z2\x1ar\x9a:br|\xa5\xe1\x83\x0fp[\t(\xc5x\x96\x1d\x93\xc2a:\xd9\x95\x9a\xe9Z\x05\x95y\xa9\xb9\xa3\x9bW\xf9\xd6\xd5a\xca\x1e\x1c\x02c\x83\xc6:}\xe3\xa1a\xa9?\x1a\x81\xb1\xbc\xb4\x9b\xd09\xc9H\x9a\x9a\x7f\x0c\xf5\xf0\xbe_5(s\x86\xd7>\x8cm\xcb\x8e\xdb\x8c\xf1\xce\xc6\xcdYl\xb7\x96\xe8\xa4\xee\x9f\xae\xeb\xcf\x91\xb3\x19\x19j\x13\xdf\xe9@\t\xe2\x83N#Pd\x16G|\xce\xf7\x85o\xb2\xa7\x84\\\xbd\x04\\\xbd\x04u\x04\x84{\xd9\x04^\x17\x856\xf6\x056\xf6\x04}\x05w\xbab\xf7\xbab\xf7\xbd\xc5\xf7\xcbn\xf7\xaf\x8c\x06\xb9\xb5\x06\xb9\xb5\x07\x0fy\x87F"\x87\x83\xc3\x86\xf9\x9d|[z|[z}\x11g\xfd\x078\xfd C\xfbv\xe8{v\xe8|Z\xb6\xfb\xa1\xc3\xfb\xa1\xc3\xfc9\x1c|[U|\x95\xb2\x83\x86\xf5\x03\x86\xf5\x03R\x12\x03j\tx%Tx%Tw\xe6b{1\x03{1\x03z2-O\xd3\xf4\xfd?O\xd45\rCOs\xdc\xf7=\xcfst\xdd7M\xf3|\xdf\x17\xc5\xd1t_\x17\xb0\xeb\xca\xf3#H\xd24\x91&Ms\\\xd75\xcb\x92\xe4\xb9D\xd14H\x00\x00\x00\x00\x01\x8b\xe8:\r\xb7\xb1\xd6\x0f\xab\xeb\x8cg\xf9\xfd\xb7\xaa\xe2\x15\xed{\x90\xedYD\xf9\x1eG\x93\xc5\x95\x11%IV\x01\x95eV^\x85D\xc5\x91d\xb3G6\xcd\xb4\xce\x9b&\xc0{\xf6\x0e', + b"\xd3\x01\xeeCP\x00L\n\xdb\xa2\x00\x00{@T\x00\x00\x00\x00\x00( \x81\x01}\xc7\xdf\xfd\xc7\xdeq\xc2\"b2\x1ar\x9a:brx\x00\x00\x00\x00\x07\x84\x99\xb1\x1a\xf3;\xc1V\xd0\xfbv\xd5\x82V\x15\xe8\x0b\xe0L{\x11\xe8\x1f\xd9`\xd3\x00\xca\x19)V\xd4\x15X\x81t\\\x97K9rI\x1d=\x01\xcf\xac\x1f\x10\x1e7\x91co\x1eS+\xe5V\xd6U\x05\x9cA\xc9\xc2\xf8\x9dy!\xd7\xe1\x1eBa\xc4\x85r\x8c\xf7'\xbfu\x10\x1fTF\xf5\xa6f\xf8\xbdoy\x178\xf0\xee\x8c\xae\xe6\xea\xf0Ag\x05lpt\x115\xb8\x13?\x01X\x84\x95\xa9\x06C\"\xe3\xe3\xde\\Rym\xa7\x92a\xfc\xcb0LwPLwPM\xfb\xd0Ni\x18L\x8d\x00Y\xfa\x90Y\xfa\x90N[\x87\x82b\x0f\x82b\x0f\x82\x98?\x83r\xd7\x81\xb4\xa0rR\xb0rR\xb0w\xae\xf8{\x19\x80~\xf3\x90vQ/\xcc\xae\x0f\xcc\xae\x0f\xd8\x0c\xe7\xd7i\xef\xd8\xfa\xa7\xbeo\x9f\xbeo\x9f\xcc\xac\x87\xc0\xf8\xa7\xc0\xf8\xa7\xcan'\xcc\x91\xb7\xd07\x88?\x10\xe0?\x10\xe0;\xc2\xb8=B'\x89\x10\xbf\x89\x10\xbf\x85!\x9f\xb9\x93\x07\xb9\x93\x07\xa9\xa5\xa4\xfd?O\xd3\xf4\xfdCP\xd44\xf7=\xcfs\xdc\xf77M\xd3t\xdf7\xcd\xf1|]\x17E\xf1{\x0e\xbc\xaf24\x8d#I\x12d\xd75\xcds\\\xb9.K\x94M\x13D\x80\x00\x00\x00\x00\x18\xbe\x83\xa0\xdb{\x1d`\xfa\xbe\xb8\xc6\x7f\x9f\xdbz\xae!^\xd7\xb9\x0e\xd5\x94O\x91\xe4y\xa1O\xec\xa6^\xcc\x08\xfb\xbaO\xbb\ntzWH\xe7w\x8e/xy\xfdA\xf2\xfc\xf8\xad}\xa98}\x9b\xe9|\xab\xdd\xfc\xcb<\xfc\xbb\xfc\xfc\xd6\xe4\xffI*\x7fK\xcb\xff\xf59\x7f\xd0M\x00\xbf\x01\x00\xdev\x82tn\x02~\xa5~\xde\xbf\xfe\xcf\tyq%y\x95\xda\xf9\x95\x9eyz\xb9\xff\x17\xfd\xff\x00w\xfds}\xfd\x90I\xfe\xa0\x91\xfe]QC\x90\xeb\xbe\xe3\xd04\x05\x03@\xcd\xb3l\xdb6\xc8R\x14\x85!D\xb1$\xfb?O\xb3\xf4\xc92D\xd1,E\x11\x00\x00\x00\x053DG\x11\x96\x07{\xd9\xb6\xae\x95\xa4c8\xf5\x8f_\xd3t\x94\xdd4\xe3x\xc5\xcbx\xe5\xd9e\xb9fU\xb5^\xfd\x0e\x13[\xe0\xb8U\x8fb\x1f\xa8?\xb8\x8b\xa9\x0c\xd2\x1e\x04C\xc9:p\x94\xe27\xbf\x97\x8f\x10B\xe0\x9f\xbe\xb1\xbdl\xfa\xb7\xfa`\x13\xb4%\xbf\x83/\t\xfe-\xbc\xe2\x807\xcbM", + b'\xd3\x01FD\x80\x00L\n\xdb\xa2\x00 \x14\x85 \x00 \x00\x00\x00 \x81\x11\x00\x7f\xff\xff\xff\xf4\xf5u\x054\xd5\xc5i\xe0\x8fg\xf2\xcd\xf39\xe0\xf61\xf7l\xf2w\x02{qG\xf77`\x01?\xc1\x93 "|\x02^<)\xbc\x8d-D\xdc\x1e\x8d\xd7\x04\xde\xbd\xce\x0fx*.\x83N`9\xfa\x83\xb9\xe8=\xce\xc9=x\x9d\xc0J\x16H\xa2\xa4\xcaM\xd8\xa5\xc9KO\xf4\xb9\xbbK\xbfd\xbew\x8c:t\xd7\xa4\x8d\xd8\x0c\xdf\x01N\x1c\xfb\xd5\x08o\xd5U\xc7\xd5\x97\x87\xd5\x8cc\xd5\xb5\xc7\xe7pg\xe0\xb5\xdf\xde\xea{\xde\x85K\xde\x19\x9c6\xe4\x9c;\x88p<~\xdc=\x97$=\xb1\xd4\x0b\x98\xe8\x0e\xec\x9c\x10w\x90\x110\xb8\x11?\xa9\x9f\xdev\xdd\xf3s\xd8\xf5FI\x80U\x14\x05/GV\xf4\xf5\xb7b\xdf\xf7`\xea\xb1\x93\xa3[Dj\tM!\xcaC\x18\x9cA5\xd0S\x80\xa1\xfc\xf3z\x0c%hI|v\x90\xed\x1d\xde\x9b\xafC\x9a\x07\xdc\xe19\xc3xH\xb53", + b"\xd3\x00\x16G\x00\x00L\n\xdb\xa2\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00&\x8d\xc9", + b"\xd3\x00\x16G\x10\x00L\n\xdb\xa0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xd979", + b"\xd3\x00\x0cL\xe0\x00\x8f\x00\x00\x00\x00\x00\x00\x00\x00\xe0?a", + b'\xd3\x00X>\x90\x00L\n\xebB\xb0\t\xafus\xe1\xde\xdf\xc3\x02\xa9\xff|\xc6\xbf\xf5A\xb8p\xbc\xeb\xdb\xfcCte\xe0\x05\xc5\x7f$\x91\x07"\x07\x8d\xdf\xc6\x04\xc2\x14\x00\xab\xdf\xf4\xc2\x17;\xffu\xbb\xfd\xf0\xb19g\xdc\x8e\x7fDN\xf1\xe5\xf0\x18\x7f\xc7:\xde\x1f\x00\x00S P\x9f.\x1f[]\xfc\xfb\xb5\x16', + b'\xd3\x00n>\xa0\x00L\n\xebB\xb0\t\xafus\xe1\xde\xdf\xd2\xeb\x03\x02\xa9\xff|\xc6\xbf\xf4leA\xb8p\xbc\xeb\xdb\xfd>dCte\xe0\x05\xc5\x7fC\xc4$\x91\x07"\x07\x8d\xdf\xd1\xf0\x06\x04\xc2\x14\x00\xab\xdf\xf4\xe9\xe4\xc2\x17;\xffu\xbb\xfd:\x8d\xf0\xb19g\xdc\x8e\x7fO\xa9DN\xf1\xe5\xf0\x18\x7f\xd3(\x87:\xde\x1f\x00\x00S%8\x80P\x9f.\x1f[]\xfd\x13\x14\xd8\xe65', ] diff --git a/src/pyrtcm/rtcmmessage.py b/src/pyrtcm/rtcmmessage.py index 4be1270..e7dd0d9 100644 --- a/src/pyrtcm/rtcmmessage.py +++ b/src/pyrtcm/rtcmmessage.py @@ -15,6 +15,7 @@ from pyrtcm.rtcmtypes_core import ( CELPRN, CELSIG, + NA, NCELL, NHARMCOEFFC, NHARMCOEFFS, @@ -264,15 +265,15 @@ def _getsatcellmaps(self): sats = {} nsat = 0 for idx in range(1, 65): - if getattr(self, "DF394") & 2 ** (64 - idx): + if getattr(self, "DF394") >> (64 - idx) & 1: nsat += 1 - sats[nsat] = prnmap.get(idx, "Reserved") + sats[nsat] = prnmap.get(idx, NA) sigs = [] nsig = 0 for idx in range(1, 33): - if getattr(self, "DF395") & 2 ** (32 - idx): - sgc = sigmap.get(idx, "Reserved") + if getattr(self, "DF395") >> (32 - idx) & 1: + sgc = sigmap.get(idx, NA) fqc = sgc[1] if sigcode else sgc[0] sigs.append(fqc) nsig += 1 @@ -283,7 +284,7 @@ def _getsatcellmaps(self): for sat in range(nsat): for sig in range(nsig): idx += 1 - if getattr(self, "DF396") & 2 ** (ncells - idx): + if getattr(self, "DF396") >> (ncells - idx) & 1: ncell += 1 cells[ncell] = (sats[sat + 1], sigs[sig]) @@ -300,6 +301,8 @@ def _getbits(self, position: int, length: int) -> int: :rtype: int """ + if length == 0: + return 0 if position + length > self._payblen: raise rte.RTCMMessageError( f"Attribute size {length} exceeds remaining " @@ -308,7 +311,7 @@ def _getbits(self, position: int, length: int) -> int: return int.from_bytes(self._payload, "big") >> ( self._payblen - position - length - ) & (2**length - 1) + ) & ((2 << length - 1) - 1) def _get_dict(self) -> dict: """ diff --git a/src/pyrtcm/rtcmtypes_core.py b/src/pyrtcm/rtcmtypes_core.py index 29c0187..2436389 100644 --- a/src/pyrtcm/rtcmtypes_core.py +++ b/src/pyrtcm/rtcmtypes_core.py @@ -46,6 +46,7 @@ ERR_RAISE = 2 ERR_LOG = 1 ERR_IGNORE = 0 +NA = "N/A" NSAT = "NSat" NSIG = "NSig" From da22086ce0f4030fd3361c50c433607d239cadd1 Mon Sep 17 00:00:00 2001 From: semuadmin <28569967+semuadmin@users.noreply.github.com> Date: Mon, 13 May 2024 07:40:24 +0100 Subject: [PATCH 6/7] update docstring --- examples/msmparser.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/examples/msmparser.py b/examples/msmparser.py index 384744e..9d48aa7 100644 --- a/examples/msmparser.py +++ b/examples/msmparser.py @@ -7,9 +7,7 @@ Each RTCM3 MSM message contains data for multiple satellites and cells -(combination of satellite and signal). The mapping between each -data item and its corresponding satellite PRN or signal ID can be performed -by pyrtcm helper functions `sat2prn` and `cell2prn`. +(combination of satellite and signal). pyrtcm parses MSM messages into a flat data structure, with repeating element names suffixed by a 2-digit index (e.g. `DF405_02`) and From b797f33c056ff53b67c09e3ca12ad5d4eb421043 Mon Sep 17 00:00:00 2001 From: semuadmin <28569967+semuadmin@users.noreply.github.com> Date: Tue, 14 May 2024 11:29:52 +0100 Subject: [PATCH 7/7] update contributor notes --- CONTRIBUTING.md | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f9828f0..59d1bf3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -20,16 +20,14 @@ If you're adding or amending rtcm payload definitions or configuration database * Avoid external library dependencies unless there's a compelling reason not to. * We use and recommend [Visual Studio Code](https://code.visualstudio.com/) with the [Python Extension](https://marketplace.visualstudio.com/items?itemName=ms-python.python) for development and testing. * Code should be documented in accordance with [Sphinx](https://www.sphinx-doc.org/en/master/) docstring conventions. -* Code should formatted using [black](https://pypi.org/project/black/) (>= 20.8). -* We use and recommend [pylint](https://pypi.org/project/pylint/) (>=2.6.0) for code analysis. -* We use and recommend [bandit](https://pypi.org/project/bandit/) (>=1.7) for security vulnerability analysis. +* Code should formatted using [black](https://pypi.org/project/black/) (>= 24.4). +* We use and recommend [pylint](https://pypi.org/project/pylint/) (>=3.0.1) for code analysis. +* We use and recommend [bandit](https://pypi.org/project/bandit/) (>=1.7.5) for security vulnerability analysis. * Commits should be [signed](https://docs.github.com/en/authentication/managing-commit-signature-verification/signing-commits). ## Testing -While we endeavour to test on as wide a variety of u-blox devices as possible, as a volunteer project we only have a limited number of devices available. We particularly welcome testing contributions relating to specialised devices (e.g. high precision HP, real-time kinematics RTK, automotive dead-reckoning ADR, etc.). - -We use python's native pytest framework for local unit testing, complemented by the GitHub Actions automated build and testing workflow. We endeavour to have 100% code coverage. +We use python's native pytest framework for local unit testing, complemented by the GitHub Actions automated build and testing workflow. We endeavour to have >98% code coverage. Please write pytest examples for new code you create and add them to the `/tests` folder following the naming convention `test_*.py`.