pygnssutils 1.2.4__tar.gz → 1.2.6__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. {pygnssutils-1.2.4/src/pygnssutils.egg-info → pygnssutils-1.2.6}/PKG-INFO +5 -5
  2. {pygnssutils-1.2.4 → pygnssutils-1.2.6}/README.md +3 -3
  3. {pygnssutils-1.2.4 → pygnssutils-1.2.6}/pyproject.toml +1 -1
  4. {pygnssutils-1.2.4 → pygnssutils-1.2.6}/src/pygnssutils/_version.py +1 -1
  5. {pygnssutils-1.2.4 → pygnssutils-1.2.6}/src/pygnssutils/globals.py +0 -4
  6. {pygnssutils-1.2.4 → pygnssutils-1.2.6}/src/pygnssutils/gnssmqttclient_cli.py +2 -1
  7. {pygnssutils-1.2.4 → pygnssutils-1.2.6}/src/pygnssutils/gnssreader.py +260 -110
  8. {pygnssutils-1.2.4 → pygnssutils-1.2.6}/src/pygnssutils/gnssstreamer.py +9 -0
  9. {pygnssutils-1.2.4 → pygnssutils-1.2.6}/src/pygnssutils/gnssstreamer_cli.py +19 -1
  10. pygnssutils-1.2.6/src/pygnssutils/rawnav.py +551 -0
  11. pygnssutils-1.2.6/src/pygnssutils/rawnav_reader.py +462 -0
  12. pygnssutils-1.2.6/src/pygnssutils/rawnav_subframes_bds.py +552 -0
  13. pygnssutils-1.2.6/src/pygnssutils/rawnav_subframes_gal.py +476 -0
  14. pygnssutils-1.2.6/src/pygnssutils/rawnav_subframes_glo.py +225 -0
  15. pygnssutils-1.2.6/src/pygnssutils/rawnav_subframes_gps.py +1045 -0
  16. pygnssutils-1.2.6/src/pygnssutils/rawnav_subframes_irn.py +183 -0
  17. pygnssutils-1.2.6/src/pygnssutils/rawnav_subframes_qzs.py +534 -0
  18. pygnssutils-1.2.6/src/pygnssutils/rawnav_subframes_sba.py +396 -0
  19. {pygnssutils-1.2.4 → pygnssutils-1.2.6}/src/pygnssutils/rinex_conv.py +116 -73
  20. {pygnssutils-1.2.4 → pygnssutils-1.2.6}/src/pygnssutils/rinex_conv_cli.py +56 -42
  21. {pygnssutils-1.2.4 → pygnssutils-1.2.6}/src/pygnssutils/rinex_conv_met.py +16 -15
  22. {pygnssutils-1.2.4 → pygnssutils-1.2.6}/src/pygnssutils/rinex_conv_nav.py +493 -82
  23. {pygnssutils-1.2.4 → pygnssutils-1.2.6}/src/pygnssutils/rinex_conv_obs.py +44 -78
  24. {pygnssutils-1.2.4 → pygnssutils-1.2.6}/src/pygnssutils/rinex_globals.py +99 -41
  25. {pygnssutils-1.2.4 → pygnssutils-1.2.6}/src/pygnssutils/rinex_helpers.py +133 -60
  26. {pygnssutils-1.2.4 → pygnssutils-1.2.6/src/pygnssutils.egg-info}/PKG-INFO +5 -5
  27. {pygnssutils-1.2.4 → pygnssutils-1.2.6}/src/pygnssutils.egg-info/SOURCES.txt +9 -4
  28. {pygnssutils-1.2.4 → pygnssutils-1.2.6}/src/pygnssutils.egg-info/requires.txt +1 -1
  29. {pygnssutils-1.2.4 → pygnssutils-1.2.6}/tests/test_rawnav.py +2 -5
  30. {pygnssutils-1.2.4 → pygnssutils-1.2.6}/tests/test_rinex.py +68 -110
  31. pygnssutils-1.2.6/tests/test_rinex_defs.py +229 -0
  32. pygnssutils-1.2.6/tests/test_rinex_parse.py +739 -0
  33. {pygnssutils-1.2.4 → pygnssutils-1.2.6}/tests/test_static.py +16 -0
  34. pygnssutils-1.2.4/src/pygnssutils/rawnav.py +0 -774
  35. pygnssutils-1.2.4/src/pygnssutils/rinex_subframes_bds.py +0 -541
  36. pygnssutils-1.2.4/src/pygnssutils/rinex_subframes_gal.py +0 -449
  37. pygnssutils-1.2.4/src/pygnssutils/rinex_subframes_glo.py +0 -136
  38. pygnssutils-1.2.4/src/pygnssutils/rinex_subframes_gps.py +0 -987
  39. pygnssutils-1.2.4/tests/test_rinex_defs.py +0 -148
  40. {pygnssutils-1.2.4 → pygnssutils-1.2.6}/LICENSE +0 -0
  41. {pygnssutils-1.2.4 → pygnssutils-1.2.6}/setup.cfg +0 -0
  42. {pygnssutils-1.2.4 → pygnssutils-1.2.6}/src/pygnssutils/__init__.py +0 -0
  43. {pygnssutils-1.2.4 → pygnssutils-1.2.6}/src/pygnssutils/exceptions.py +0 -0
  44. {pygnssutils-1.2.4 → pygnssutils-1.2.6}/src/pygnssutils/gnssmqttclient.py +0 -0
  45. {pygnssutils-1.2.4 → pygnssutils-1.2.6}/src/pygnssutils/gnssntripclient.py +0 -0
  46. {pygnssutils-1.2.4 → pygnssutils-1.2.6}/src/pygnssutils/gnssntripclient_cli.py +0 -0
  47. {pygnssutils-1.2.4 → pygnssutils-1.2.6}/src/pygnssutils/gnssserver.py +0 -0
  48. {pygnssutils-1.2.4 → pygnssutils-1.2.6}/src/pygnssutils/gnssserver_cli.py +0 -0
  49. {pygnssutils-1.2.4 → pygnssutils-1.2.6}/src/pygnssutils/helpers.py +0 -0
  50. {pygnssutils-1.2.4 → pygnssutils-1.2.6}/src/pygnssutils/mqttmessage.py +0 -0
  51. {pygnssutils-1.2.4 → pygnssutils-1.2.6}/src/pygnssutils/socket_server.py +0 -0
  52. {pygnssutils-1.2.4 → pygnssutils-1.2.6}/src/pygnssutils.egg-info/dependency_links.txt +0 -0
  53. {pygnssutils-1.2.4 → pygnssutils-1.2.6}/src/pygnssutils.egg-info/entry_points.txt +0 -0
  54. {pygnssutils-1.2.4 → pygnssutils-1.2.6}/src/pygnssutils.egg-info/top_level.txt +0 -0
  55. {pygnssutils-1.2.4 → pygnssutils-1.2.6}/tests/test_cli.py +0 -0
  56. {pygnssutils-1.2.4 → pygnssutils-1.2.6}/tests/test_gnssstreamer.py +0 -0
  57. {pygnssutils-1.2.4 → pygnssutils-1.2.6}/tests/test_socketwrapper.py +0 -0
  58. {pygnssutils-1.2.4 → pygnssutils-1.2.6}/tests/test_sourcetable.py +0 -0
  59. {pygnssutils-1.2.4 → pygnssutils-1.2.6}/tests/test_stream.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pygnssutils
3
- Version: 1.2.4
3
+ Version: 1.2.6
4
4
  Summary: GNSS Command Line Utilities
5
5
  Author-email: Steve Smith <semuadmin@semuconsulting.com>
6
6
  Maintainer-email: Steve Smith <semuadmin@semuconsulting.com>
@@ -34,7 +34,7 @@ Requires-Dist: certifi>=2025.0.0
34
34
  Requires-Dist: paho-mqtt>=2.1.0
35
35
  Requires-Dist: pyserial>=3.5
36
36
  Requires-Dist: pyspartn>=1.0.8
37
- Requires-Dist: pyubx2>=1.3.3
37
+ Requires-Dist: pyubx2>=1.3.6
38
38
  Requires-Dist: pynmeagps>=1.1.5
39
39
  Requires-Dist: pysbf2>=1.0.4
40
40
  Requires-Dist: pyubxutils>=1.0.6
@@ -328,7 +328,7 @@ Streaming terminated, 93 messages processed with 0 errors.
328
328
 
329
329
  ### <a name="gnssstreamerservice">Installing gnssstreamer as a systemd service on Linux</a>
330
330
 
331
- This example should work for most Linux distributions running `systemd` and `python3>=3.8`, including Raspberry Pi OS (*substitute `dnf` for `apt` as necessary*).
331
+ 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*).
332
332
 
333
333
  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*).
334
334
 
@@ -543,11 +543,11 @@ class pygnssutils.rinex_conv.RinexConvertor(app, rinex_version, rinex_type, gnss
543
543
 
544
544
  A command line utility and Python class `RinexConverter` to convert binary GNSS data logs to RINEX text file format.
545
545
 
546
- **NB: RINEX conversion is an experimental work in progress (*contributions and feedback welcome*). The current ALPHA release implements the following functionality:**
546
+ **NB: RINEX conversion is an experimental work in progress (*contributions and feedback welcome*). The current release implements the following functionality:**
547
547
 
548
548
  1. RINEX versions 3.05 and 4.02.
549
549
  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.
550
- 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.
550
+ 1. Convert binary RXM-SFRBX (navigation subframe) data from u-blox receivers to RINEX Navigation file format.
551
551
  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.
552
552
  1. Convert NMEA MWD (wind speed and direction) and XDR (temperature and pressure) sensor data to RINEX Meteorology file format.
553
553
 
@@ -284,7 +284,7 @@ Streaming terminated, 93 messages processed with 0 errors.
284
284
 
285
285
  ### <a name="gnssstreamerservice">Installing gnssstreamer as a systemd service on Linux</a>
286
286
 
287
- This example should work for most Linux distributions running `systemd` and `python3>=3.8`, including Raspberry Pi OS (*substitute `dnf` for `apt` as necessary*).
287
+ 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*).
288
288
 
289
289
  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*).
290
290
 
@@ -499,11 +499,11 @@ class pygnssutils.rinex_conv.RinexConvertor(app, rinex_version, rinex_type, gnss
499
499
 
500
500
  A command line utility and Python class `RinexConverter` to convert binary GNSS data logs to RINEX text file format.
501
501
 
502
- **NB: RINEX conversion is an experimental work in progress (*contributions and feedback welcome*). The current ALPHA release implements the following functionality:**
502
+ **NB: RINEX conversion is an experimental work in progress (*contributions and feedback welcome*). The current release implements the following functionality:**
503
503
 
504
504
  1. RINEX versions 3.05 and 4.02.
505
505
  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.
506
- 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.
506
+ 1. Convert binary RXM-SFRBX (navigation subframe) data from u-blox receivers to RINEX Navigation file format.
507
507
  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.
508
508
  1. Convert NMEA MWD (wind speed and direction) and XDR (temperature and pressure) sensor data to RINEX Meteorology file format.
509
509
 
@@ -38,7 +38,7 @@ dependencies = [
38
38
  "paho-mqtt>=2.1.0",
39
39
  "pyserial>=3.5",
40
40
  "pyspartn>=1.0.8",
41
- "pyubx2>=1.3.3",
41
+ "pyubx2>=1.3.6",
42
42
  "pynmeagps>=1.1.5",
43
43
  "pysbf2>=1.0.4",
44
44
  "pyubxutils>=1.0.6",
@@ -8,4 +8,4 @@ Created on 2 Oct 2020
8
8
  :license: BSD 3-Clause
9
9
  """
10
10
 
11
- __version__ = "1.2.4"
11
+ __version__ = "1.2.6"
@@ -206,10 +206,6 @@ ALLNMEA = "allnmea"
206
206
  ALLUBX = "allubx"
207
207
  MINNMEA = "minnmea"
208
208
  MINUBX = "minubx"
209
- ALLNMEA_CLS = [b"\xf0", b"\xf1"]
210
- MINMMEA_ID = [b"\xf0\x00", b"\xf0\x02", b"\xf0\x03", b"\xf0\x04", b"\xf0\x05"]
211
- ALLUBX_CLS = [b"\x01"]
212
- MINUBX_ID = [b"\x01\x04", b"\x01\x07", b"\x01\x35"]
213
209
  PYGNSSUTILS_CRT = f"{HOME}/pygnssutils.crt"
214
210
  """Name of default TLS CRT file"""
215
211
  PYGNSSUTILS_CRTPATH = "PYGNSSUTILS_CRTPATH"
@@ -65,7 +65,8 @@ def main():
65
65
  clientid = getenv(ENV_MQTT_CLIENTID, default="enter-client-id")
66
66
  ap = ArgumentParser(
67
67
  description="Client ID can be read from environment variable MQTTCLIENTID",
68
- epilog=EPILOG,
68
+ epilog=EPILOG
69
+ + " NB: This utility is DEPRECATED and will be removed in the next release of pygnssutils (1.2.7)",
69
70
  formatter_class=ArgumentDefaultsHelpFormatter,
70
71
  )
71
72
  ap.add_argument("-V", "--version", action="version", version="%(prog)s " + VERSION)
@@ -1,18 +1,20 @@
1
1
  """
2
2
  gnssreader.py
3
3
 
4
- Generic GNSS class.
4
+ Generic GNSS reader / parser class.
5
5
 
6
- Reads and parses individual UBX, SBF, QGC, NMEA or RTCM3 messages from any viable
7
- data stream which supports a read(n) -> bytes method.
6
+ Reads and parses individual UBX, SBF, QGC, UNI, NMEA or RTCM3 messages from any viable
7
+ GNSS data stream which supports a read(n) -> bytes method.
8
8
 
9
9
  It is essentially an amalgamation of the Reader classes in the separate pyubx2, pynmeagps,
10
- pyrtcm, pysbf2 and pyqgc packages.
10
+ pyrtcm, pysbf2, pyqgc and pyunigps packages.
11
11
 
12
12
  Returns both the raw binary data (as bytes) and the parsed data.
13
13
 
14
- - 'protfilter' governs which protocols (NMEA, UBX, SBF, QGC or RTCM3) are processed
15
- - 'quitonerror' governs how errors are handled
14
+ - 'protfilter' governs which protocols (NMEA, UBX, SBF, QGC, UNI or RTCM3) are processed.
15
+ - 'msgfilter' governs which individual message types are processed.
16
+ - `parsing` governs whether payloads are fully or partially parsed.
17
+ - 'quitonerror' governs how errors are handled.
16
18
  - 'msgmode' indicates the type of UBX datastream (output GET, input SET, query POLL).
17
19
  If msgmode is set to SETPOLL, input/query mode will be automatically detected by parser.
18
20
 
@@ -28,7 +30,7 @@ Created on 6 Oct 2025
28
30
  from logging import getLogger
29
31
  from socket import socket
30
32
  from types import FunctionType, NoneType
31
- from typing import Literal
33
+ from typing import Any, Literal
32
34
 
33
35
  from pynmeagps import (
34
36
  NMEA_HDR,
@@ -81,6 +83,7 @@ from pyubx2 import (
81
83
  UBXReader,
82
84
  UBXStreamError,
83
85
  UBXTypeError,
86
+ escapeall,
84
87
  )
85
88
  from pyunigps import (
86
89
  UNI_HDR,
@@ -93,6 +96,7 @@ from pyunigps import (
93
96
  )
94
97
 
95
98
  from pygnssutils.exceptions import GNSSStreamError
99
+ from pygnssutils.globals import DEFAULT_BUFSIZE
96
100
 
97
101
  NMEA_PROTOCOL = 1
98
102
  """NMEA Protocol"""
@@ -106,6 +110,108 @@ QGC_PROTOCOL = 16
106
110
  """QGC Protocol (Quectel)"""
107
111
  UNI_PROTOCOL = 32
108
112
  """UNI Protocol (Unicore)"""
113
+ PARSE_NONE = 0
114
+ """No Parsing, raw output only"""
115
+ PARSE_FULL = 1
116
+ """Full parsing of all attributes"""
117
+ PARSE_META = 2
118
+ """Parse protocol and message Id only"""
119
+
120
+ PROTOCOLS = {
121
+ NMEA_PROTOCOL: "NMEA",
122
+ UBX_PROTOCOL: "UBX",
123
+ RTCM3_PROTOCOL: "RTCM",
124
+ SBF_PROTOCOL: "SBF",
125
+ QGC_PROTOCOL: "QGC",
126
+ UNI_PROTOCOL: "UNI",
127
+ }
128
+
129
+
130
+ class GNSSMessage:
131
+ """
132
+ Generic GNSSMessage class.
133
+
134
+ Holds partially parsed GNSS binary messages (`parsing=2`).
135
+ """
136
+
137
+ def __init__(self, protocol: int, msgid: int | str, data: bytes):
138
+ """
139
+ Constructor
140
+
141
+ :param int protocol: GNSS protocol e.g. 2 (=UBX)
142
+ :param int | str msgid: GNSS message id e.g. 0x0215 or "GNGSA"
143
+ :param bytes data: raw data from original message
144
+ """
145
+
146
+ self._protocol = protocol
147
+ self._identity = msgid
148
+ self._data = data
149
+
150
+ def __str__(self) -> str:
151
+ """
152
+ Human readable representation.
153
+
154
+ :return: human readable representation
155
+ :rtype: str
156
+ """
157
+
158
+ prot = PROTOCOLS.get(self._protocol, "UNKNOWN")
159
+ ident = (
160
+ f"0x{self._identity:04x}"
161
+ if self._protocol in (UBX_PROTOCOL, QGC_PROTOCOL)
162
+ else self._identity
163
+ )
164
+ return f"<{prot}({ident}, length={self.length}, data={escapeall(self._data)})>"
165
+
166
+ def __repr__(self) -> str:
167
+ """
168
+ Machine readable representation.
169
+
170
+ eval(repr(obj)) = obj
171
+
172
+ :return: machine readable representation
173
+ :rtype: str
174
+
175
+ """
176
+
177
+ ident = (
178
+ f"'{self._identity}'"
179
+ if isinstance(self._identity, str)
180
+ else f"{self._identity}"
181
+ )
182
+ return f"GNSSMessage({self._protocol}, {ident}, {self._data})"
183
+
184
+ @property
185
+ def protocol(self) -> int:
186
+ """
187
+ Getter for protocol.
188
+ """
189
+
190
+ return self._protocol
191
+
192
+ @property
193
+ def identity(self) -> int | str:
194
+ """
195
+ Getter for identity.
196
+ """
197
+
198
+ return self._identity
199
+
200
+ @property
201
+ def data(self) -> bytes:
202
+ """
203
+ Getter for raw data.
204
+ """
205
+
206
+ return self._data
207
+
208
+ @property
209
+ def length(self) -> int:
210
+ """
211
+ Getter for length of original raw data.
212
+ """
213
+
214
+ return len(self._data)
109
215
 
110
216
 
111
217
  class GNSSReader:
@@ -127,8 +233,9 @@ class GNSSReader:
127
233
  quitonerror: Literal[0, 1, 2] = ERR_LOG,
128
234
  parsebitfield: bool = True,
129
235
  labelmsm: Literal[0, 1] = 1,
130
- bufsize: int = 4096,
131
- parsing: bool = True,
236
+ bufsize: int = DEFAULT_BUFSIZE,
237
+ parsing: Literal[0, 1, 2] = PARSE_FULL,
238
+ msgfilter: tuple | int | str = "",
132
239
  errorhandler: FunctionType | NoneType = None,
133
240
  ):
134
241
  """Constructor.
@@ -138,13 +245,15 @@ class GNSSReader:
138
245
  :param int validate: VALCKSUM (1) = Validate checksum,
139
246
  VALNONE (0) = ignore invalid checksum (1)
140
247
  :param int protfilter: NMEA_PROTOCOL (1), UBX_PROTOCOL (2), RTCM3_PROTOCOL (4),
141
- SBF_PROTOCOL (8), QGC_PROTOCOL (16), UNI_PROTOCOL (32). Can be OR'd (7)
248
+ SBF_PROTOCOL (8), QGC_PROTOCOL (16), UNI_PROTOCOL (32). Can be OR'd (63)
142
249
  :param Literal[0,1,2] quitonerror: ERR_IGNORE (0) = ignore errors, \
143
250
  ERR_LOG (1) = log continue, ERR_RAISE (2) = (re)raise (1)
144
251
  :param bool parsebitfield: 1 = parse bitfields, 0 = leave as bytes (1)
145
252
  :param Literal[0,1] labelmsm: RTCM3 MSM label type 1 = RINEX, 2 = BAND (1)
146
253
  :param int bufsize: socket recv buffer size (4096)
147
- :param bool parsing: True = parse data, False = don't parse data (output raw only) (True)
254
+ :param Literal[0,1,2] parsing: PARSE_NONE (0) = no parsing (raw only), \
255
+ PARSE_FULL (1) = full parsing, PARSE_META (2) = parse metadata only (1)
256
+ :param tuple | int | str msgfilter: parsed message filter ("" = ALL)
148
257
  :param FunctionType | NoneType errorhandler: error handling object or function (None)
149
258
  :raises: UBXStreamError (if mode is invalid)
150
259
  """
@@ -162,6 +271,10 @@ class GNSSReader:
162
271
  self._labelmsm = labelmsm
163
272
  self._msgmode = msgmode
164
273
  self._parsing = parsing
274
+ self._msgfilter = (
275
+ (msgfilter,) if not isinstance(msgfilter, tuple) else msgfilter
276
+ )
277
+ self._filtermsg = self._msgfilter not in ((), ("",))
165
278
  self._logger = getLogger(__name__)
166
279
 
167
280
  if self._msgmode not in (GET, SET, POLL, SETPOLL):
@@ -174,12 +287,12 @@ class GNSSReader:
174
287
 
175
288
  return self
176
289
 
177
- def __next__(self) -> tuple:
290
+ def __next__(self) -> tuple[bytes | NoneType, Any]:
178
291
  """
179
292
  Return next item in iteration.
180
293
 
181
294
  :return: tuple of (raw_data as bytes, parsed_data as UBXMessage)
182
- :rtype: tuple
295
+ :rtype: tuple[bytes | NoneType, Any]
183
296
  :raises: StopIteration
184
297
 
185
298
  """
@@ -187,22 +300,24 @@ class GNSSReader:
187
300
  raw_data, parsed_data = self.read()
188
301
  if raw_data is None and parsed_data is None:
189
302
  raise StopIteration
190
- return (raw_data, parsed_data)
303
+ return raw_data, parsed_data
191
304
 
192
- def read(self) -> tuple:
305
+ def read(self) -> tuple[bytes | NoneType, Any]:
193
306
  """
194
- Read a single NMEA, UBX, SBF, QGC or RTCM3 message from the stream buffer
307
+ Read a single NMEA, UBX, SBF, QGC, UNI or RTCM3 message from the stream buffer
195
308
  and return both raw and parsed data.
196
309
 
197
310
  'protfilter' determines which protocols are parsed.
198
311
  'quitonerror' determines whether to raise, log or ignore parsing errors.
199
312
 
200
313
  :return: tuple of (raw_data as bytes, parsed_data as NMEAMessage, UBXMessage,
201
- SBFMessage, QGCMessage or RTCMMessage)
202
- :rtype: tuple
314
+ SBFMessage, QGCMessage, UNIMessage, RTCMMessage or (if `parsing=2`) GNSSMessage)
315
+ :rtype: tuple[bytes | NoneType, Any]
203
316
  :raises: Exception (if invalid or unrecognised protocol in data stream)
204
317
  """
205
318
 
319
+ raw_data = None
320
+ parsed_data = None
206
321
  parsing = True
207
322
  while parsing: # loop until end of valid message or EOF
208
323
  try:
@@ -274,7 +389,6 @@ class GNSSReader:
274
389
  parsing = False
275
390
  else:
276
391
  continue
277
- # unrecognised protocol header
278
392
  else:
279
393
  raise GNSSStreamError(f"Unknown protocol header {bytehdr}.")
280
394
 
@@ -311,173 +425,209 @@ class GNSSReader:
311
425
  self._do_error(err)
312
426
  continue
313
427
 
314
- return (raw_data, parsed_data)
428
+ return raw_data, parsed_data
315
429
 
316
- def _parse_ubx(self, hdr: bytes) -> tuple[bytes, UBXMessage | NoneType]:
430
+ def _parse_ubx(
431
+ self, hdr: bytes
432
+ ) -> tuple[bytes, UBXMessage | GNSSMessage | NoneType]:
317
433
  """
318
434
  Parse UBX message (using pyubx2).
319
435
 
320
436
  :param bytes hdr: UBX header (b'\\xb5\\x62')
321
437
  :return: tuple of (raw_data as bytes, parsed_data as UBXMessage or None)
322
- :rtype: tuple[bytes, UBXMessage | NoneType]
438
+ :rtype: tuple[bytes, UBXMessage | GNSSMessage | NoneType]
323
439
  """
324
440
 
325
441
  # read the rest of the UBX message from the buffer
326
442
  byten = self._read_bytes(4)
327
- clsid = byten[0:1]
328
- msgid = byten[1:2]
443
+ msgid = byten[0:2]
444
+ msgidi = int.from_bytes(msgid, "big") # UBX msgids are documented in big endian
329
445
  lenb = byten[2:4]
330
446
  leni = int.from_bytes(lenb, "little", signed=False)
331
- byten = self._read_bytes(leni + 2)
332
- plb = byten[0:leni]
333
- cksum = byten[leni : leni + 2]
334
- raw_data = hdr + clsid + msgid + lenb + plb + cksum
447
+ paycrc = self._read_bytes(leni + 2)
448
+ raw_data = hdr + msgid + lenb + paycrc
335
449
  # only parse if we need to (filter passes UBX)
336
- if (self._protfilter & UBX_PROTOCOL) and self._parsing:
337
- parsed_data = UBXReader.parse(
338
- raw_data,
339
- validate=self._validate,
340
- msgmode=self._msgmode,
341
- parsebitfield=self._parsebf,
342
- )
343
- else:
344
- parsed_data = None
345
- return (raw_data, parsed_data)
346
-
347
- def _parse_sbf(self, hdr: bytes) -> tuple[bytes, SBFMessage | NoneType]:
450
+ parsed_data = None
451
+ if self._protfilter & UBX_PROTOCOL and (
452
+ not self._filtermsg or msgidi in self._msgfilter
453
+ ):
454
+ if self._parsing == PARSE_FULL:
455
+ parsed_data = UBXReader.parse(
456
+ raw_data,
457
+ validate=self._validate,
458
+ msgmode=self._msgmode,
459
+ parsebitfield=self._parsebf,
460
+ )
461
+ elif self._parsing == PARSE_META:
462
+ parsed_data = GNSSMessage(UBX_PROTOCOL, msgidi, raw_data)
463
+ return raw_data, parsed_data
464
+
465
+ def _parse_sbf(
466
+ self, hdr: bytes
467
+ ) -> tuple[bytes, SBFMessage | GNSSMessage | NoneType]:
348
468
  """
349
469
  Parse SBF message (using pysbf2).
350
470
 
351
471
  :param bytes hdr: SBF header (b'\\x24\\x40')
352
472
  :return: tuple of (raw_data as bytes, parsed_data as SBFMessage or None)
353
- :rtype: tuple[bytes, SBFMessage | NoneType]
473
+ :rtype: tuple[bytes, SBFMessage | GNSSMessage | NoneType]
354
474
  """
355
475
 
356
476
  # read the rest of the SBF message from the buffer
357
477
  byten = self._read_bytes(6)
358
478
  crc = byten[0:2]
359
479
  msgid = byten[2:4]
480
+ msgidi = int.from_bytes(msgid, "little") & 0x1FFF
360
481
  lenb = byten[4:6]
361
482
  # lenb includes 8 byte header
362
483
  leni = int.from_bytes(lenb, "little", signed=False) - 8
363
484
  plb = self._read_bytes(leni)
364
485
  raw_data = hdr + crc + msgid + lenb + plb
365
486
  # only parse if we need to (filter passes SBF)
366
- if (self._protfilter & SBF_PROTOCOL) and self._parsing:
367
- parsed_data = SBFReader.parse(
368
- raw_data,
369
- validate=self._validate,
370
- parsebitfield=self._parsebf,
371
- )
372
- else:
373
- parsed_data = None
374
- return (raw_data, parsed_data)
375
-
376
- def _parse_uni(self, hdr: bytes) -> tuple[bytes, UNIMessage | NoneType]:
487
+ parsed_data = None
488
+ if self._protfilter & SBF_PROTOCOL and (
489
+ not self._filtermsg or msgidi in self._msgfilter
490
+ ):
491
+ if self._parsing == PARSE_FULL:
492
+ parsed_data = SBFReader.parse(
493
+ raw_data,
494
+ validate=self._validate,
495
+ parsebitfield=self._parsebf,
496
+ )
497
+ elif self._parsing == PARSE_META:
498
+ parsed_data = GNSSMessage(SBF_PROTOCOL, msgidi, raw_data)
499
+ return raw_data, parsed_data
500
+
501
+ def _parse_uni(
502
+ self, hdr: bytes
503
+ ) -> tuple[bytes, UNIMessage | GNSSMessage | NoneType]:
377
504
  """
378
505
  Parse binary UNI message.
379
506
 
380
507
  :param bytes hdr: UNI header (b'\\xaa\\x44\\xb5')
381
508
  :return: tuple of (raw_data as bytes, parsed_data as UNIMessage or None)
382
- :rtype: tuple[bytes, UNIMessage | NoneType]
509
+ :rtype: tuple[bytes, UNIMessage | GNSSMessage | NoneType]
383
510
  """
384
511
 
385
512
  header = self._read_bytes(21)
386
- lenp = int.from_bytes(header[3:5], "little")
387
- payload = self._read_bytes(lenp + 4)
513
+ leni = int.from_bytes(header[3:5], "little")
514
+ payload = self._read_bytes(leni + 4)
515
+ msgidi = int.from_bytes(header[1:3], "little")
388
516
  raw_data = hdr + header + payload
389
517
  # only parse if we need to (filter passes UNI)
390
- if (self._protfilter & UNI_PROTOCOL) and self._parsing:
391
- parsed_data = UNIReader.parse(
392
- raw_data,
393
- msgmode=self._msgmode,
394
- validate=self._validate,
395
- parsebitfield=self._parsebf,
396
- )
397
- else:
398
- parsed_data = None
399
- return (raw_data, parsed_data)
400
-
401
- def _parse_qgc(self, hdr: bytes) -> tuple[bytes, QGCMessage | NoneType]:
518
+ parsed_data = None
519
+ if self._protfilter & UNI_PROTOCOL and (
520
+ not self._filtermsg or msgidi in self._msgfilter
521
+ ):
522
+ if self._parsing == PARSE_FULL:
523
+ parsed_data = UNIReader.parse(
524
+ raw_data,
525
+ validate=self._validate,
526
+ msgmode=self._msgmode,
527
+ parsebitfield=self._parsebf,
528
+ )
529
+ elif self._parsing == PARSE_META:
530
+ parsed_data = GNSSMessage(UNI_PROTOCOL, msgidi, raw_data)
531
+ return raw_data, parsed_data
532
+
533
+ def _parse_qgc(
534
+ self, hdr: bytes
535
+ ) -> tuple[bytes, QGCMessage | GNSSMessage | NoneType]:
402
536
  """
403
537
  Parse QGC message (using pyqgc).
404
538
 
405
539
  :param bytes hdr: QGC header (b'\\x51\\x47')
406
540
  :return: tuple of (raw_data as bytes, parsed_data as QGCMessage or None)
407
- :rtype: tuple[bytes, QGCMessage | NoneType]
541
+ :rtype: tuple[bytes, QGCMessage | GNSSMessage | NoneType]
408
542
  """
409
543
 
410
544
  # read the rest of the QGC message from the buffer
411
545
  byten = self._read_bytes(4)
412
- msggrp = byten[0:1]
413
- msgid = byten[1:2]
546
+ msgid = byten[0:2]
547
+ msgidi = int.from_bytes(msgid, "big") # QCG msgids are documented in big endian
414
548
  lenb = byten[2:4]
415
549
  leni = int.from_bytes(lenb, "little", signed=False)
416
- byten = self._read_bytes(leni + 2)
417
- plb = byten[0:leni]
418
- cksum = byten[leni : leni + 2]
419
- raw_data = hdr + msggrp + msgid + lenb + plb + cksum
550
+ paycrc = self._read_bytes(leni + 2)
551
+ raw_data = hdr + msgid + lenb + paycrc
420
552
  # only parse if we need to (filter passes QGC)
421
- if (self._protfilter & QGC_PROTOCOL) and self._parsing:
422
- parsed_data = QGCReader.parse(
423
- raw_data,
424
- msgmode=self._msgmode,
425
- validate=self._validate,
426
- parsebitfield=self._parsebf,
427
- )
428
- else:
429
- parsed_data = None
430
- return (raw_data, parsed_data)
431
-
432
- def _parse_nmea(self, hdr: bytes) -> tuple[bytes, NMEAMessage | NoneType]:
553
+ parsed_data = None
554
+ if self._protfilter & QGC_PROTOCOL and (
555
+ not self._filtermsg or msgidi in self._msgfilter
556
+ ):
557
+ if self._parsing == PARSE_FULL:
558
+ parsed_data = QGCReader.parse(
559
+ raw_data,
560
+ validate=self._validate,
561
+ msgmode=self._msgmode,
562
+ parsebitfield=self._parsebf,
563
+ )
564
+ elif self._parsing == PARSE_META:
565
+ parsed_data = GNSSMessage(QGC_PROTOCOL, msgidi, raw_data)
566
+ return raw_data, parsed_data
567
+
568
+ def _parse_nmea(
569
+ self, hdr: bytes
570
+ ) -> tuple[bytes, NMEAMessage | GNSSMessage | NoneType]:
433
571
  """
434
572
  Parse NMEA message (using pynmeagps).
435
573
 
436
574
  :param bytes hdr: NMEA header (b'\\x24\\x..')
437
575
  :return: tuple of (raw_data as bytes, parsed_data as NMEAMessage or None)
438
- :rtype: tuple[bytes, NMEAMessage | NoneType]
576
+ :rtype: tuple[bytes, NMEAMessage | GNSSMessage | NoneType]
439
577
  """
440
578
 
441
579
  # read the rest of the NMEA message from the buffer
442
580
  byten = self._read_line() # NMEA protocol is CRLF-terminated
443
581
  raw_data = hdr + byten
582
+ msgids = (
583
+ raw_data[1:].decode("utf-8", errors="backslashreplace").split(",", 1)[0]
584
+ )
444
585
  # only parse if we need to (filter passes NMEA)
445
- if (self._protfilter & NMEA_PROTOCOL) and self._parsing:
446
- # invoke pynmeagps parser
447
- parsed_data = NMEAReader.parse(
448
- raw_data,
449
- validate=self._validate,
450
- msgmode=self._msgmode,
451
- )
452
- else:
453
- parsed_data = None
454
- return (raw_data, parsed_data)
455
-
456
- def _parse_rtcm3(self, hdr: bytes) -> tuple[bytes, RTCMMessage | NoneType]:
586
+ parsed_data = None
587
+ if self._protfilter & NMEA_PROTOCOL and (
588
+ not self._filtermsg or msgids in self._msgfilter
589
+ ):
590
+ if self._parsing == PARSE_FULL:
591
+ parsed_data = NMEAReader.parse(
592
+ raw_data,
593
+ validate=self._validate,
594
+ msgmode=self._msgmode,
595
+ )
596
+ elif self._parsing == PARSE_META:
597
+ parsed_data = GNSSMessage(NMEA_PROTOCOL, msgids, raw_data)
598
+ return raw_data, parsed_data
599
+
600
+ def _parse_rtcm3(
601
+ self, hdr: bytes
602
+ ) -> tuple[bytes, RTCMMessage | GNSSMessage | NoneType]:
457
603
  """
458
604
  Parse RTCM3 message (using pyrtcm).
459
605
 
460
606
  :param bytes hdr: first 2 bytes of RTCM3 header
461
607
  :return: tuple of (raw_data as bytes, parsed_stub as RTCMMessage)
462
- :rtype: tuple[bytes, RTCMMessage | NoneType]
608
+ :rtype: tuple[bytes, RTCMMessage | GNSSMessage | NoneType]
463
609
  """
464
610
 
465
611
  hdr3 = self._read_bytes(1)
466
612
  size = hdr3[0] | (hdr[1] << 8)
467
613
  payload = self._read_bytes(size)
614
+ msgidi = ((int.from_bytes(payload[0:2], "big")) >> 4) & 0xFFF
468
615
  crc = self._read_bytes(3)
469
616
  raw_data = hdr + hdr3 + payload + crc
470
617
  # only parse if we need to (filter passes RTCM)
471
- if (self._protfilter & RTCM3_PROTOCOL) and self._parsing:
472
- # invoke pyrtcm parser
473
- parsed_data = RTCMReader.parse(
474
- raw_data,
475
- validate=self._validate,
476
- labelmsm=self._labelmsm,
477
- )
478
- else:
479
- parsed_data = None
480
- return (raw_data, parsed_data)
618
+ parsed_data = None
619
+ if self._protfilter & RTCM3_PROTOCOL and (
620
+ not self._filtermsg or msgidi in self._msgfilter
621
+ ):
622
+ if self._parsing == PARSE_FULL:
623
+ parsed_data = RTCMReader.parse(
624
+ raw_data,
625
+ validate=self._validate,
626
+ labelmsm=self._labelmsm,
627
+ )
628
+ elif self._parsing == PARSE_META:
629
+ parsed_data = GNSSMessage(RTCM3_PROTOCOL, msgidi, raw_data)
630
+ return raw_data, parsed_data
481
631
 
482
632
  def _read_bytes(self, size: int) -> bytes:
483
633
  """