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.
- {pygnssutils-1.2.4/src/pygnssutils.egg-info → pygnssutils-1.2.6}/PKG-INFO +5 -5
- {pygnssutils-1.2.4 → pygnssutils-1.2.6}/README.md +3 -3
- {pygnssutils-1.2.4 → pygnssutils-1.2.6}/pyproject.toml +1 -1
- {pygnssutils-1.2.4 → pygnssutils-1.2.6}/src/pygnssutils/_version.py +1 -1
- {pygnssutils-1.2.4 → pygnssutils-1.2.6}/src/pygnssutils/globals.py +0 -4
- {pygnssutils-1.2.4 → pygnssutils-1.2.6}/src/pygnssutils/gnssmqttclient_cli.py +2 -1
- {pygnssutils-1.2.4 → pygnssutils-1.2.6}/src/pygnssutils/gnssreader.py +260 -110
- {pygnssutils-1.2.4 → pygnssutils-1.2.6}/src/pygnssutils/gnssstreamer.py +9 -0
- {pygnssutils-1.2.4 → pygnssutils-1.2.6}/src/pygnssutils/gnssstreamer_cli.py +19 -1
- pygnssutils-1.2.6/src/pygnssutils/rawnav.py +551 -0
- pygnssutils-1.2.6/src/pygnssutils/rawnav_reader.py +462 -0
- pygnssutils-1.2.6/src/pygnssutils/rawnav_subframes_bds.py +552 -0
- pygnssutils-1.2.6/src/pygnssutils/rawnav_subframes_gal.py +476 -0
- pygnssutils-1.2.6/src/pygnssutils/rawnav_subframes_glo.py +225 -0
- pygnssutils-1.2.6/src/pygnssutils/rawnav_subframes_gps.py +1045 -0
- pygnssutils-1.2.6/src/pygnssutils/rawnav_subframes_irn.py +183 -0
- pygnssutils-1.2.6/src/pygnssutils/rawnav_subframes_qzs.py +534 -0
- pygnssutils-1.2.6/src/pygnssutils/rawnav_subframes_sba.py +396 -0
- {pygnssutils-1.2.4 → pygnssutils-1.2.6}/src/pygnssutils/rinex_conv.py +116 -73
- {pygnssutils-1.2.4 → pygnssutils-1.2.6}/src/pygnssutils/rinex_conv_cli.py +56 -42
- {pygnssutils-1.2.4 → pygnssutils-1.2.6}/src/pygnssutils/rinex_conv_met.py +16 -15
- {pygnssutils-1.2.4 → pygnssutils-1.2.6}/src/pygnssutils/rinex_conv_nav.py +493 -82
- {pygnssutils-1.2.4 → pygnssutils-1.2.6}/src/pygnssutils/rinex_conv_obs.py +44 -78
- {pygnssutils-1.2.4 → pygnssutils-1.2.6}/src/pygnssutils/rinex_globals.py +99 -41
- {pygnssutils-1.2.4 → pygnssutils-1.2.6}/src/pygnssutils/rinex_helpers.py +133 -60
- {pygnssutils-1.2.4 → pygnssutils-1.2.6/src/pygnssutils.egg-info}/PKG-INFO +5 -5
- {pygnssutils-1.2.4 → pygnssutils-1.2.6}/src/pygnssutils.egg-info/SOURCES.txt +9 -4
- {pygnssutils-1.2.4 → pygnssutils-1.2.6}/src/pygnssutils.egg-info/requires.txt +1 -1
- {pygnssutils-1.2.4 → pygnssutils-1.2.6}/tests/test_rawnav.py +2 -5
- {pygnssutils-1.2.4 → pygnssutils-1.2.6}/tests/test_rinex.py +68 -110
- pygnssutils-1.2.6/tests/test_rinex_defs.py +229 -0
- pygnssutils-1.2.6/tests/test_rinex_parse.py +739 -0
- {pygnssutils-1.2.4 → pygnssutils-1.2.6}/tests/test_static.py +16 -0
- pygnssutils-1.2.4/src/pygnssutils/rawnav.py +0 -774
- pygnssutils-1.2.4/src/pygnssutils/rinex_subframes_bds.py +0 -541
- pygnssutils-1.2.4/src/pygnssutils/rinex_subframes_gal.py +0 -449
- pygnssutils-1.2.4/src/pygnssutils/rinex_subframes_glo.py +0 -136
- pygnssutils-1.2.4/src/pygnssutils/rinex_subframes_gps.py +0 -987
- pygnssutils-1.2.4/tests/test_rinex_defs.py +0 -148
- {pygnssutils-1.2.4 → pygnssutils-1.2.6}/LICENSE +0 -0
- {pygnssutils-1.2.4 → pygnssutils-1.2.6}/setup.cfg +0 -0
- {pygnssutils-1.2.4 → pygnssutils-1.2.6}/src/pygnssutils/__init__.py +0 -0
- {pygnssutils-1.2.4 → pygnssutils-1.2.6}/src/pygnssutils/exceptions.py +0 -0
- {pygnssutils-1.2.4 → pygnssutils-1.2.6}/src/pygnssutils/gnssmqttclient.py +0 -0
- {pygnssutils-1.2.4 → pygnssutils-1.2.6}/src/pygnssutils/gnssntripclient.py +0 -0
- {pygnssutils-1.2.4 → pygnssutils-1.2.6}/src/pygnssutils/gnssntripclient_cli.py +0 -0
- {pygnssutils-1.2.4 → pygnssutils-1.2.6}/src/pygnssutils/gnssserver.py +0 -0
- {pygnssutils-1.2.4 → pygnssutils-1.2.6}/src/pygnssutils/gnssserver_cli.py +0 -0
- {pygnssutils-1.2.4 → pygnssutils-1.2.6}/src/pygnssutils/helpers.py +0 -0
- {pygnssutils-1.2.4 → pygnssutils-1.2.6}/src/pygnssutils/mqttmessage.py +0 -0
- {pygnssutils-1.2.4 → pygnssutils-1.2.6}/src/pygnssutils/socket_server.py +0 -0
- {pygnssutils-1.2.4 → pygnssutils-1.2.6}/src/pygnssutils.egg-info/dependency_links.txt +0 -0
- {pygnssutils-1.2.4 → pygnssutils-1.2.6}/src/pygnssutils.egg-info/entry_points.txt +0 -0
- {pygnssutils-1.2.4 → pygnssutils-1.2.6}/src/pygnssutils.egg-info/top_level.txt +0 -0
- {pygnssutils-1.2.4 → pygnssutils-1.2.6}/tests/test_cli.py +0 -0
- {pygnssutils-1.2.4 → pygnssutils-1.2.6}/tests/test_gnssstreamer.py +0 -0
- {pygnssutils-1.2.4 → pygnssutils-1.2.6}/tests/test_socketwrapper.py +0 -0
- {pygnssutils-1.2.4 → pygnssutils-1.2.6}/tests/test_sourcetable.py +0 -0
- {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.
|
|
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.
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
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
|
|
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.
|
|
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
|
|
|
@@ -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
|
|
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
|
-
- '
|
|
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 =
|
|
131
|
-
parsing:
|
|
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 (
|
|
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
|
|
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
|
|
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
|
|
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
|
|
428
|
+
return raw_data, parsed_data
|
|
315
429
|
|
|
316
|
-
def _parse_ubx(
|
|
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
|
-
|
|
328
|
-
|
|
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
|
-
|
|
332
|
-
|
|
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
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
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
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
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
|
-
|
|
387
|
-
payload = self._read_bytes(
|
|
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
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
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
|
-
|
|
413
|
-
|
|
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
|
-
|
|
417
|
-
|
|
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
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
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
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
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
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
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
|
"""
|