pygnssutils 1.2.8__tar.gz → 1.3.0__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.8/src/pygnssutils.egg-info → pygnssutils-1.3.0}/PKG-INFO +8 -8
- {pygnssutils-1.2.8 → pygnssutils-1.3.0}/pyproject.toml +8 -8
- {pygnssutils-1.2.8 → pygnssutils-1.3.0}/src/pygnssutils/_version.py +1 -1
- {pygnssutils-1.2.8 → pygnssutils-1.3.0}/src/pygnssutils/gnssntripclient.py +94 -85
- {pygnssutils-1.2.8 → pygnssutils-1.3.0}/src/pygnssutils/gnssreader.py +15 -14
- {pygnssutils-1.2.8 → pygnssutils-1.3.0}/src/pygnssutils/helpers.py +3 -3
- {pygnssutils-1.2.8 → pygnssutils-1.3.0}/src/pygnssutils/rinex_conv.py +0 -1
- {pygnssutils-1.2.8 → pygnssutils-1.3.0}/src/pygnssutils/rinex_conv_met.py +4 -4
- {pygnssutils-1.2.8 → pygnssutils-1.3.0/src/pygnssutils.egg-info}/PKG-INFO +8 -8
- pygnssutils-1.3.0/src/pygnssutils.egg-info/requires.txt +10 -0
- {pygnssutils-1.2.8 → pygnssutils-1.3.0}/tests/test_static.py +2 -2
- pygnssutils-1.2.8/src/pygnssutils.egg-info/requires.txt +0 -10
- {pygnssutils-1.2.8 → pygnssutils-1.3.0}/LICENSE +0 -0
- {pygnssutils-1.2.8 → pygnssutils-1.3.0}/README.md +0 -0
- {pygnssutils-1.2.8 → pygnssutils-1.3.0}/setup.cfg +0 -0
- {pygnssutils-1.2.8 → pygnssutils-1.3.0}/src/pygnssutils/__init__.py +0 -0
- {pygnssutils-1.2.8 → pygnssutils-1.3.0}/src/pygnssutils/exceptions.py +0 -0
- {pygnssutils-1.2.8 → pygnssutils-1.3.0}/src/pygnssutils/globals.py +0 -0
- {pygnssutils-1.2.8 → pygnssutils-1.3.0}/src/pygnssutils/gnssntripclient_cli.py +0 -0
- {pygnssutils-1.2.8 → pygnssutils-1.3.0}/src/pygnssutils/gnssserver.py +0 -0
- {pygnssutils-1.2.8 → pygnssutils-1.3.0}/src/pygnssutils/gnssserver_cli.py +0 -0
- {pygnssutils-1.2.8 → pygnssutils-1.3.0}/src/pygnssutils/gnssstreamer.py +0 -0
- {pygnssutils-1.2.8 → pygnssutils-1.3.0}/src/pygnssutils/gnssstreamer_cli.py +0 -0
- {pygnssutils-1.2.8 → pygnssutils-1.3.0}/src/pygnssutils/rawnav.py +0 -0
- {pygnssutils-1.2.8 → pygnssutils-1.3.0}/src/pygnssutils/rawnav_reader.py +0 -0
- {pygnssutils-1.2.8 → pygnssutils-1.3.0}/src/pygnssutils/rawnav_subframes_bds.py +0 -0
- {pygnssutils-1.2.8 → pygnssutils-1.3.0}/src/pygnssutils/rawnav_subframes_gal.py +0 -0
- {pygnssutils-1.2.8 → pygnssutils-1.3.0}/src/pygnssutils/rawnav_subframes_glo.py +0 -0
- {pygnssutils-1.2.8 → pygnssutils-1.3.0}/src/pygnssutils/rawnav_subframes_gps.py +0 -0
- {pygnssutils-1.2.8 → pygnssutils-1.3.0}/src/pygnssutils/rawnav_subframes_irn.py +0 -0
- {pygnssutils-1.2.8 → pygnssutils-1.3.0}/src/pygnssutils/rawnav_subframes_qzs.py +0 -0
- {pygnssutils-1.2.8 → pygnssutils-1.3.0}/src/pygnssutils/rawnav_subframes_sba.py +0 -0
- {pygnssutils-1.2.8 → pygnssutils-1.3.0}/src/pygnssutils/rinex_conv_cli.py +0 -0
- {pygnssutils-1.2.8 → pygnssutils-1.3.0}/src/pygnssutils/rinex_conv_nav.py +0 -0
- {pygnssutils-1.2.8 → pygnssutils-1.3.0}/src/pygnssutils/rinex_conv_obs.py +0 -0
- {pygnssutils-1.2.8 → pygnssutils-1.3.0}/src/pygnssutils/rinex_globals.py +0 -0
- {pygnssutils-1.2.8 → pygnssutils-1.3.0}/src/pygnssutils/rinex_helpers.py +0 -0
- {pygnssutils-1.2.8 → pygnssutils-1.3.0}/src/pygnssutils/socket_server.py +0 -0
- {pygnssutils-1.2.8 → pygnssutils-1.3.0}/src/pygnssutils.egg-info/SOURCES.txt +0 -0
- {pygnssutils-1.2.8 → pygnssutils-1.3.0}/src/pygnssutils.egg-info/dependency_links.txt +0 -0
- {pygnssutils-1.2.8 → pygnssutils-1.3.0}/src/pygnssutils.egg-info/entry_points.txt +0 -0
- {pygnssutils-1.2.8 → pygnssutils-1.3.0}/src/pygnssutils.egg-info/top_level.txt +0 -0
- {pygnssutils-1.2.8 → pygnssutils-1.3.0}/tests/test_cli.py +0 -0
- {pygnssutils-1.2.8 → pygnssutils-1.3.0}/tests/test_gnssstreamer.py +0 -0
- {pygnssutils-1.2.8 → pygnssutils-1.3.0}/tests/test_rawnav.py +0 -0
- {pygnssutils-1.2.8 → pygnssutils-1.3.0}/tests/test_rinex.py +0 -0
- {pygnssutils-1.2.8 → pygnssutils-1.3.0}/tests/test_rinex_defs.py +0 -0
- {pygnssutils-1.2.8 → pygnssutils-1.3.0}/tests/test_rinex_parse.py +0 -0
- {pygnssutils-1.2.8 → pygnssutils-1.3.0}/tests/test_socketwrapper.py +0 -0
- {pygnssutils-1.2.8 → pygnssutils-1.3.0}/tests/test_sourcetable.py +0 -0
- {pygnssutils-1.2.8 → pygnssutils-1.3.0}/tests/test_stream.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pygnssutils
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.3.0
|
|
4
4
|
Summary: GNSS Command Line Utilities
|
|
5
5
|
Author: Steve Smith
|
|
6
6
|
Author-email: semuadmin@semuconsulting.com
|
|
@@ -34,15 +34,15 @@ Requires-Python: >=3.10
|
|
|
34
34
|
Description-Content-Type: text/markdown
|
|
35
35
|
License-File: LICENSE
|
|
36
36
|
Requires-Dist: certifi>=2026.0.0
|
|
37
|
-
Requires-Dist: pynmeagps>=1.1.
|
|
38
|
-
Requires-Dist: pyqgc>=1.0.
|
|
39
|
-
Requires-Dist: pyrtcm>=1.2.
|
|
40
|
-
Requires-Dist: pysbf2>=1.0.
|
|
37
|
+
Requires-Dist: pynmeagps>=1.1.8
|
|
38
|
+
Requires-Dist: pyqgc>=1.0.1
|
|
39
|
+
Requires-Dist: pyrtcm>=1.2.1
|
|
40
|
+
Requires-Dist: pysbf2>=1.0.7
|
|
41
41
|
Requires-Dist: pyserial>=3.5
|
|
42
|
-
Requires-Dist: pyspartn>=1.0
|
|
43
|
-
Requires-Dist: pyubx2>=1.3.
|
|
42
|
+
Requires-Dist: pyspartn>=1.1.0
|
|
43
|
+
Requires-Dist: pyubx2>=1.3.9
|
|
44
44
|
Requires-Dist: pyubxutils>=1.0.6
|
|
45
|
-
Requires-Dist: pyunigps>=1.0.
|
|
45
|
+
Requires-Dist: pyunigps>=1.0.1
|
|
46
46
|
Dynamic: license-file
|
|
47
47
|
|
|
48
48
|
pygnssutils
|
|
@@ -39,15 +39,15 @@ classifiers = [
|
|
|
39
39
|
|
|
40
40
|
dependencies = [
|
|
41
41
|
"certifi>=2026.0.0",
|
|
42
|
-
"pynmeagps>=1.1.
|
|
43
|
-
"pyqgc>=1.0.
|
|
44
|
-
"pyrtcm>=1.2.
|
|
45
|
-
"pysbf2>=1.0.
|
|
42
|
+
"pynmeagps>=1.1.8",
|
|
43
|
+
"pyqgc>=1.0.1",
|
|
44
|
+
"pyrtcm>=1.2.1",
|
|
45
|
+
"pysbf2>=1.0.7",
|
|
46
46
|
"pyserial>=3.5",
|
|
47
|
-
"pyspartn>=1.0
|
|
48
|
-
"pyubx2>=1.3.
|
|
47
|
+
"pyspartn>=1.1.0",
|
|
48
|
+
"pyubx2>=1.3.9",
|
|
49
49
|
"pyubxutils>=1.0.6",
|
|
50
|
-
"pyunigps>=1.0.
|
|
50
|
+
"pyunigps>=1.0.1",
|
|
51
51
|
]
|
|
52
52
|
|
|
53
53
|
[project.scripts]
|
|
@@ -66,7 +66,7 @@ changelog = "https://github.com/semuconsulting/pygnssutils/blob/master/RELEASE_N
|
|
|
66
66
|
build = [
|
|
67
67
|
"build",
|
|
68
68
|
"packaging>=24.2",
|
|
69
|
-
"pip",
|
|
69
|
+
"pip>=25.1",
|
|
70
70
|
"setuptools>=75.0.0",
|
|
71
71
|
"twine>=6.1.0",
|
|
72
72
|
"wheel",
|
|
@@ -27,21 +27,28 @@ Created on 03 Jun 2022
|
|
|
27
27
|
:license: BSD 3-Clause
|
|
28
28
|
"""
|
|
29
29
|
|
|
30
|
-
import socket
|
|
31
|
-
import ssl
|
|
32
30
|
from base64 import b64encode
|
|
33
31
|
from datetime import datetime, timedelta, timezone
|
|
34
32
|
from io import BufferedWriter, TextIOWrapper
|
|
35
33
|
from logging import getLogger
|
|
36
34
|
from os import getenv
|
|
37
35
|
from queue import Queue
|
|
36
|
+
from socket import (
|
|
37
|
+
SHUT_RDWR,
|
|
38
|
+
create_connection,
|
|
39
|
+
gaierror,
|
|
40
|
+
gethostbyname,
|
|
41
|
+
socket,
|
|
42
|
+
timeout,
|
|
43
|
+
)
|
|
44
|
+
from ssl import PROTOCOL_TLS_CLIENT, SSLCertVerificationError, SSLContext, SSLError
|
|
38
45
|
from threading import Event, Thread
|
|
46
|
+
from types import NoneType
|
|
47
|
+
from typing import Any
|
|
39
48
|
|
|
40
49
|
from certifi import where as findcacerts
|
|
41
50
|
from pynmeagps import GET, NMEAMessage, SocketWrapper
|
|
42
|
-
from
|
|
43
|
-
from pyspartn import SPARTNMessageError, SPARTNParseError, SPARTNReader, SPARTNTypeError
|
|
44
|
-
from pyubx2 import ERR_LOG, RTCM3_PROTOCOL, UBXReader
|
|
51
|
+
from pyspartn import SPARTNReader
|
|
45
52
|
from serial import Serial
|
|
46
53
|
|
|
47
54
|
from pygnssutils._version import __version__ as VERSION
|
|
@@ -70,6 +77,7 @@ from pygnssutils.globals import (
|
|
|
70
77
|
PYGNSSUTILS_CRTPATH,
|
|
71
78
|
VERBOSITY_MEDIUM,
|
|
72
79
|
)
|
|
80
|
+
from pygnssutils.gnssreader import ERR_LOG, RTCM3_PROTOCOL, GNSSReader
|
|
73
81
|
from pygnssutils.helpers import find_mp_distance, ipprot2int, set_logging
|
|
74
82
|
|
|
75
83
|
TIMEOUT = 3
|
|
@@ -91,13 +99,13 @@ class GNSSNTRIPClient:
|
|
|
91
99
|
|
|
92
100
|
def __init__(
|
|
93
101
|
self,
|
|
94
|
-
app=None,
|
|
102
|
+
app: Any | NoneType = None,
|
|
95
103
|
**kwargs,
|
|
96
104
|
):
|
|
97
105
|
"""
|
|
98
106
|
Constructor.
|
|
99
107
|
|
|
100
|
-
:param
|
|
108
|
+
:param Any | NoneType app: application from which this class is invoked (None)
|
|
101
109
|
:param int retries: (kwarg) maximum failed connection retries (5)
|
|
102
110
|
:param int retryinterval: (kwarg) retry interval in seconds (10)
|
|
103
111
|
:param int timeout: (kwarg) inactivity timeout in seconds (10)
|
|
@@ -185,7 +193,7 @@ class GNSSNTRIPClient:
|
|
|
185
193
|
:param bool spartndecode: (kwarg) decode SPARTN messages (0)
|
|
186
194
|
:param str | NoneType spartnkey: (kwarg) SPARTN decryption key (None)
|
|
187
195
|
:param str | datetime datetime: (kwarg) SPARTN decryption basedate (now(utc))
|
|
188
|
-
:param
|
|
196
|
+
:param Any output: (kwarg) writeable output medium (serial, file, socket, queue) (None)
|
|
189
197
|
:param Event stopevent: (kwarg) stopevent to terminate `run()` (internal `Event()`)
|
|
190
198
|
:returns: boolean flag 0 = stream terminated, 1 = streaming data
|
|
191
199
|
:rtype: bool
|
|
@@ -221,8 +229,8 @@ class GNSSNTRIPClient:
|
|
|
221
229
|
daemon=True,
|
|
222
230
|
).start()
|
|
223
231
|
if self.settings["mountpoint"] != "":
|
|
224
|
-
return
|
|
225
|
-
return
|
|
232
|
+
return True
|
|
233
|
+
return False
|
|
226
234
|
|
|
227
235
|
def stop(self):
|
|
228
236
|
"""
|
|
@@ -237,7 +245,7 @@ class GNSSNTRIPClient:
|
|
|
237
245
|
self,
|
|
238
246
|
settings: dict,
|
|
239
247
|
stopevent: Event,
|
|
240
|
-
output:
|
|
248
|
+
output: Any,
|
|
241
249
|
):
|
|
242
250
|
"""
|
|
243
251
|
Main read thread.
|
|
@@ -247,7 +255,7 @@ class GNSSNTRIPClient:
|
|
|
247
255
|
|
|
248
256
|
:param dict settings: settings as dictionary
|
|
249
257
|
:param Event stopevent: stop event
|
|
250
|
-
:param
|
|
258
|
+
:param Any output: output stream for raw data
|
|
251
259
|
"""
|
|
252
260
|
|
|
253
261
|
self._retrycount = 0
|
|
@@ -271,8 +279,8 @@ class GNSSNTRIPClient:
|
|
|
271
279
|
ConnectionRefusedError,
|
|
272
280
|
ConnectionResetError,
|
|
273
281
|
OverflowError,
|
|
274
|
-
|
|
275
|
-
|
|
282
|
+
gaierror,
|
|
283
|
+
timeout, # subclass of OSError in Python<=3.9
|
|
276
284
|
TimeoutError,
|
|
277
285
|
) as err:
|
|
278
286
|
errm = str(repr(err))
|
|
@@ -286,14 +294,14 @@ class GNSSNTRIPClient:
|
|
|
286
294
|
)
|
|
287
295
|
self._app_update_status(True, (errm, ERRCOL))
|
|
288
296
|
# critical errors...
|
|
289
|
-
except (
|
|
290
|
-
errc = err.strerror
|
|
291
|
-
if "certificate is not valid for 'www." in err.strerror:
|
|
297
|
+
except (SSLError, SSLCertVerificationError) as err:
|
|
298
|
+
errc = str(err.strerror)
|
|
299
|
+
if "certificate is not valid for 'www." in str(err.strerror):
|
|
292
300
|
errc += (
|
|
293
301
|
f" - try using '{hostname[4:]}' rather than "
|
|
294
302
|
f"'{hostname}' for the NTRIP caster URL"
|
|
295
303
|
)
|
|
296
|
-
elif "unable to get local issuer certificate" in err.strerror:
|
|
304
|
+
elif "unable to get local issuer certificate" in str(err.strerror):
|
|
297
305
|
errc += f" - try adding the NTRIP caster URL SSL certificate to {findcacerts()}"
|
|
298
306
|
except OSError: # socket already closed, ignore
|
|
299
307
|
errc = "socket closed"
|
|
@@ -322,12 +330,12 @@ class GNSSNTRIPClient:
|
|
|
322
330
|
"""
|
|
323
331
|
|
|
324
332
|
hostname = settings["server"]
|
|
325
|
-
sock =
|
|
326
|
-
(
|
|
333
|
+
sock = create_connection(
|
|
334
|
+
(gethostbyname(hostname), int(settings["port"])),
|
|
327
335
|
timeout=self._timeout,
|
|
328
336
|
)
|
|
329
337
|
if int(settings["https"]):
|
|
330
|
-
context =
|
|
338
|
+
context = SSLContext(PROTOCOL_TLS_CLIENT)
|
|
331
339
|
context.load_verify_locations(findcacerts())
|
|
332
340
|
if int(settings.get("selfsign", 0)):
|
|
333
341
|
# context.verify_mode = ssl.CERT_NONE
|
|
@@ -336,16 +344,17 @@ class GNSSNTRIPClient:
|
|
|
336
344
|
sock = context.wrap_socket(sock, server_hostname=hostname)
|
|
337
345
|
return sock
|
|
338
346
|
|
|
339
|
-
def _close_connection(self, sock: socket):
|
|
347
|
+
def _close_connection(self, sock: socket | NoneType):
|
|
340
348
|
"""
|
|
341
349
|
Close socket connection.
|
|
342
350
|
|
|
343
|
-
:param socket sock: open socket
|
|
351
|
+
:param socket | NoneType sock: open socket
|
|
344
352
|
"""
|
|
345
353
|
|
|
346
354
|
try:
|
|
347
|
-
sock
|
|
348
|
-
|
|
355
|
+
if sock is not None:
|
|
356
|
+
sock.shutdown(SHUT_RDWR)
|
|
357
|
+
sock.close()
|
|
349
358
|
except (AttributeError, OSError): # already closed, ignore
|
|
350
359
|
pass
|
|
351
360
|
|
|
@@ -354,14 +363,14 @@ class GNSSNTRIPClient:
|
|
|
354
363
|
sock: socket,
|
|
355
364
|
settings: dict,
|
|
356
365
|
stopevent: Event,
|
|
357
|
-
output:
|
|
366
|
+
output: Any,
|
|
358
367
|
) -> int:
|
|
359
368
|
"""
|
|
360
369
|
Send HTTP request to NTRIP server and process incoming data.
|
|
361
370
|
|
|
362
371
|
:param dict settings: settings as dictionary
|
|
363
372
|
:param Event stopevent: stop event
|
|
364
|
-
:param
|
|
373
|
+
:param Any output: output stream for raw data
|
|
365
374
|
:returns rc: return code (0 - stop, 1 - ok)
|
|
366
375
|
:rtype: int
|
|
367
376
|
:raises: Various socket error types if connection fails
|
|
@@ -497,12 +506,21 @@ class GNSSNTRIPClient:
|
|
|
497
506
|
)
|
|
498
507
|
return bdy
|
|
499
508
|
|
|
509
|
+
def _error_handler(self, err: Exception):
|
|
510
|
+
"""
|
|
511
|
+
Error handler.
|
|
512
|
+
|
|
513
|
+
:param Exception err: exception
|
|
514
|
+
"""
|
|
515
|
+
|
|
516
|
+
self.logger.debug(err)
|
|
517
|
+
|
|
500
518
|
def _parse_ntrip_data(
|
|
501
519
|
self,
|
|
502
520
|
sock: socket,
|
|
503
521
|
settings: dict,
|
|
504
522
|
stopevent: Event,
|
|
505
|
-
output:
|
|
523
|
+
output: Any,
|
|
506
524
|
):
|
|
507
525
|
"""
|
|
508
526
|
Read and parse incoming NTRIP RTCM3/SPARTN data stream.
|
|
@@ -510,6 +528,7 @@ class GNSSNTRIPClient:
|
|
|
510
528
|
:param socket sock: raw socket
|
|
511
529
|
:param dict settings: settings as dictionary
|
|
512
530
|
:param Event stopevent: stop event
|
|
531
|
+
:param Any output: output medium
|
|
513
532
|
:raises: TimeoutError if inactivity timeout exceeded
|
|
514
533
|
"""
|
|
515
534
|
|
|
@@ -528,44 +547,29 @@ class GNSSNTRIPClient:
|
|
|
528
547
|
decode=settings.get("spartndecode", False),
|
|
529
548
|
key=settings.get("spartnkey", "ABCD1234"),
|
|
530
549
|
basedate=settings.get("spartnbasedate", 0),
|
|
550
|
+
errorhandler=self._error_handler,
|
|
531
551
|
)
|
|
532
552
|
else:
|
|
533
|
-
parser =
|
|
553
|
+
parser = GNSSReader(
|
|
534
554
|
stream,
|
|
535
555
|
protfilter=RTCM3_PROTOCOL,
|
|
536
556
|
quitonerror=ERR_LOG,
|
|
537
557
|
bufsize=DEFAULT_BUFSIZE,
|
|
538
|
-
labelmsm=
|
|
558
|
+
labelmsm=1,
|
|
559
|
+
errorhandler=self._error_handler,
|
|
539
560
|
)
|
|
540
561
|
|
|
541
562
|
while not stopevent.is_set():
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
if
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
)
|
|
548
|
-
|
|
549
|
-
f"Inactivity timeout error after {self._timeout} seconds"
|
|
550
|
-
)
|
|
551
|
-
else:
|
|
552
|
-
if hasattr(parsed_data, "identity"):
|
|
553
|
-
self.logger.debug(f"Message received: {parsed_data.identity}")
|
|
554
|
-
self._do_output(output, raw_data, parsed_data)
|
|
555
|
-
last_activity = datetime.now()
|
|
556
|
-
self._send_gga(sock, settings["ggainterval"], output)
|
|
557
|
-
|
|
558
|
-
except (
|
|
559
|
-
RTCMMessageError,
|
|
560
|
-
RTCMParseError,
|
|
561
|
-
RTCMTypeError,
|
|
562
|
-
SPARTNMessageError,
|
|
563
|
-
SPARTNParseError,
|
|
564
|
-
SPARTNTypeError,
|
|
565
|
-
) as err:
|
|
566
|
-
parsed_data = f"Error parsing data stream {err}"
|
|
563
|
+
raw_data, parsed_data = parser.read()
|
|
564
|
+
if raw_data is None:
|
|
565
|
+
if datetime.now() - last_activity > timedelta(seconds=self._timeout):
|
|
566
|
+
raise TimeoutError(
|
|
567
|
+
f"Inactivity timeout error after {self._timeout} seconds"
|
|
568
|
+
)
|
|
569
|
+
else:
|
|
567
570
|
self._do_output(output, raw_data, parsed_data)
|
|
568
|
-
|
|
571
|
+
last_activity = datetime.now()
|
|
572
|
+
self._send_gga(sock, settings["ggainterval"], output)
|
|
569
573
|
|
|
570
574
|
def _parse_sourcetable(self, response: str) -> list:
|
|
571
575
|
"""
|
|
@@ -576,10 +580,10 @@ class GNSSNTRIPClient:
|
|
|
576
580
|
:rtype: list
|
|
577
581
|
"""
|
|
578
582
|
|
|
579
|
-
self.logger.
|
|
583
|
+
self.logger.debug(f"Sourcetable:\n{response=}")
|
|
580
584
|
sourcetable = []
|
|
581
|
-
|
|
582
|
-
for line in
|
|
585
|
+
respons = response.split("\r\n")
|
|
586
|
+
for line in respons:
|
|
583
587
|
if line.find("STR;") >= 0: # mountpoint entry
|
|
584
588
|
strbits = line.split(";")
|
|
585
589
|
if strbits[0] == "STR":
|
|
@@ -601,9 +605,9 @@ class GNSSNTRIPClient:
|
|
|
601
605
|
for i, col in enumerate(row):
|
|
602
606
|
dlm = "," if i < len(row) - 1 else "\r\n"
|
|
603
607
|
srt += f"{col}{dlm}"
|
|
604
|
-
return
|
|
608
|
+
return bytes(srt, "utf-8")
|
|
605
609
|
|
|
606
|
-
def _format_gga(self) -> tuple:
|
|
610
|
+
def _format_gga(self) -> tuple[bytes | NoneType, NMEAMessage | NoneType]:
|
|
607
611
|
"""
|
|
608
612
|
Format NMEA GGA sentence using pynmeagps. The raw string
|
|
609
613
|
output is suitable for sending to an NTRIP socket.
|
|
@@ -611,8 +615,7 @@ class GNSSNTRIPClient:
|
|
|
611
615
|
derived from fix string.
|
|
612
616
|
|
|
613
617
|
:returns: tuple of (raw NMEA message as bytes, NMEAMessage)
|
|
614
|
-
:rtype: tuple
|
|
615
|
-
:rtype: tuple
|
|
618
|
+
:rtype: tuple[bytes | NoneType, NMEAMessage | NoneType]
|
|
616
619
|
"""
|
|
617
620
|
|
|
618
621
|
try:
|
|
@@ -639,36 +642,35 @@ class GNSSNTRIPClient:
|
|
|
639
642
|
diffStation=diffstation,
|
|
640
643
|
)
|
|
641
644
|
|
|
642
|
-
|
|
643
|
-
return raw_data, parsed_data
|
|
645
|
+
return parsed_data.serialize(), parsed_data
|
|
644
646
|
except ValueError:
|
|
645
647
|
return None, None
|
|
646
648
|
|
|
647
|
-
def _send_gga(self, sock: socket, ggainterval: int, output:
|
|
649
|
+
def _send_gga(self, sock: socket, ggainterval: int, output: Any):
|
|
648
650
|
"""
|
|
649
651
|
Send NMEA GGA sentence to NTRIP server at prescribed interval.
|
|
650
652
|
|
|
651
653
|
:param socket sock: open socket
|
|
652
654
|
:param int ggainterval: GGA send interval in seconds (-1 = don't send)
|
|
653
|
-
:param
|
|
655
|
+
:param Any output: writeable output medium e.g. serial port
|
|
654
656
|
"""
|
|
655
657
|
|
|
656
658
|
if ggainterval != NOGGA:
|
|
657
659
|
if datetime.now() > self._last_gga + timedelta(seconds=ggainterval):
|
|
658
660
|
raw_data, parsed_data = self._format_gga()
|
|
659
|
-
if parsed_data is not None:
|
|
661
|
+
if raw_data is not None and parsed_data is not None:
|
|
660
662
|
sock.sendall(raw_data)
|
|
661
663
|
self._do_output(output, raw_data, parsed_data)
|
|
662
664
|
self._last_gga = datetime.now()
|
|
663
665
|
|
|
664
|
-
def _get_closest_mountpoint(self) -> tuple:
|
|
666
|
+
def _get_closest_mountpoint(self) -> tuple[str | NoneType, float | NoneType]:
|
|
665
667
|
"""
|
|
666
668
|
THREADED
|
|
667
669
|
Find closest mountpoint in sourcetable
|
|
668
670
|
if valid reference lat/lon are available.
|
|
669
671
|
|
|
670
672
|
:returns: tuple of (mountpoint, distance)
|
|
671
|
-
:rtype: tuple
|
|
673
|
+
:rtype: tuple[str | NoneType, float | NoneType]
|
|
672
674
|
"""
|
|
673
675
|
|
|
674
676
|
try:
|
|
@@ -678,7 +680,7 @@ class GNSSNTRIPClient:
|
|
|
678
680
|
)
|
|
679
681
|
if self._settings["mountpoint"] == "":
|
|
680
682
|
self._settings["mountpoint"] = closest_mp
|
|
681
|
-
self.logger.
|
|
683
|
+
self.logger.debug(
|
|
682
684
|
"Closest mountpoint to reference location "
|
|
683
685
|
f"({lat}, {lon}) = {closest_mp}, {dist} km."
|
|
684
686
|
)
|
|
@@ -687,28 +689,33 @@ class GNSSNTRIPClient:
|
|
|
687
689
|
return None, None
|
|
688
690
|
return closest_mp, dist
|
|
689
691
|
|
|
690
|
-
def _do_output(self, output:
|
|
692
|
+
def _do_output(self, output: Any, raw: bytes | NoneType, parsed: Any | NoneType):
|
|
691
693
|
"""
|
|
692
694
|
Send sourcetable/closest mountpoint or RTCM3/SPARTN data to designated output medium.
|
|
693
695
|
|
|
694
696
|
If output is Queue, will send both raw and parsed data.
|
|
695
697
|
|
|
696
|
-
:param
|
|
697
|
-
:param bytes raw: raw data
|
|
698
|
-
:param
|
|
698
|
+
:param Any output: writeable output medium for raw data
|
|
699
|
+
:param bytes | NoneType raw: raw data
|
|
700
|
+
:param Any | NoneType parsed: parsed message
|
|
699
701
|
"""
|
|
700
702
|
|
|
703
|
+
if hasattr(parsed, "identity"):
|
|
704
|
+
self.logger.debug(f"Message received: {type(parsed)} {parsed.identity}")
|
|
705
|
+
|
|
701
706
|
if output is not None:
|
|
702
707
|
# serialize sourcetable if outputting to stream
|
|
703
708
|
if isinstance(raw, list) and not isinstance(output, Queue):
|
|
704
709
|
raw = self._serialize_sourcetable(raw)
|
|
705
|
-
if isinstance(output, (Serial, BufferedWriter)):
|
|
710
|
+
if isinstance(output, (Serial, BufferedWriter)) and raw is not None:
|
|
706
711
|
output.write(raw)
|
|
707
|
-
|
|
712
|
+
output.flush()
|
|
713
|
+
elif isinstance(output, TextIOWrapper) and parsed is not None:
|
|
708
714
|
output.write(str(parsed))
|
|
709
|
-
|
|
715
|
+
output.flush()
|
|
716
|
+
elif isinstance(output, Queue) and raw is not None:
|
|
710
717
|
output.put(raw if self.__app == CLIAPP else (raw, parsed))
|
|
711
|
-
elif isinstance(output, socket
|
|
718
|
+
elif isinstance(output, socket) and raw is not None:
|
|
712
719
|
output.sendall(raw)
|
|
713
720
|
|
|
714
721
|
# notify any calling app that data is available
|
|
@@ -716,18 +723,18 @@ class GNSSNTRIPClient:
|
|
|
716
723
|
if hasattr(self.__app, "set_event"):
|
|
717
724
|
self.__app.set_event(NTRIP_EVENT)
|
|
718
725
|
|
|
719
|
-
def _app_update_status(self, status: bool, msgt: tuple
|
|
726
|
+
def _app_update_status(self, status: bool, msgt: tuple[str, str]):
|
|
720
727
|
"""
|
|
721
728
|
Update NTRIP connection status in calling application.
|
|
722
729
|
|
|
723
730
|
:param bool status: NTRIP server connection status
|
|
724
|
-
:param tuple msgt: (message, color)
|
|
731
|
+
:param tuple[str,str] msgt: (message, color)
|
|
725
732
|
"""
|
|
726
733
|
|
|
727
734
|
if msgt[1] == ERRCOL:
|
|
728
735
|
self.logger.error(msgt[0])
|
|
729
736
|
else:
|
|
730
|
-
self.logger.
|
|
737
|
+
self.logger.debug(msgt[0])
|
|
731
738
|
if self.__app is not None:
|
|
732
739
|
if hasattr(self.__app, "dialog"):
|
|
733
740
|
dlg = self.__app.dialog(DLGTNTRIP)
|
|
@@ -866,15 +873,17 @@ class GNSSNTRIPClient:
|
|
|
866
873
|
return self._response_headers.get("content-type", "")
|
|
867
874
|
|
|
868
875
|
@property
|
|
869
|
-
def response_body(self) ->
|
|
876
|
+
def response_body(self) -> bytes | str | NoneType:
|
|
870
877
|
"""
|
|
871
878
|
Get response body if available.
|
|
872
879
|
|
|
873
880
|
:returns: response body as bytes or string, depending on encoding
|
|
874
|
-
:rtype:
|
|
881
|
+
:rtype: bytes | str | NoneType
|
|
875
882
|
"""
|
|
876
883
|
|
|
877
|
-
if
|
|
884
|
+
if isinstance(self._response_body, bytes) and (
|
|
885
|
+
"text/" in self.content_type or self.is_sourcetable
|
|
886
|
+
):
|
|
878
887
|
return self._response_body.decode()
|
|
879
888
|
return self._response_body
|
|
880
889
|
|
|
@@ -29,7 +29,7 @@ Created on 6 Oct 2025
|
|
|
29
29
|
|
|
30
30
|
from logging import getLogger
|
|
31
31
|
from socket import socket
|
|
32
|
-
from types import FunctionType, NoneType
|
|
32
|
+
from types import FunctionType, MethodType, NoneType
|
|
33
33
|
from typing import Any, Literal
|
|
34
34
|
|
|
35
35
|
from pynmeagps import (
|
|
@@ -236,7 +236,7 @@ class GNSSReader:
|
|
|
236
236
|
bufsize: int = DEFAULT_BUFSIZE,
|
|
237
237
|
parsing: Literal[0, 1, 2] = PARSE_FULL,
|
|
238
238
|
msgfilter: tuple | int | str = "",
|
|
239
|
-
errorhandler: FunctionType | NoneType = None,
|
|
239
|
+
errorhandler: FunctionType | MethodType | NoneType = None,
|
|
240
240
|
):
|
|
241
241
|
"""Constructor.
|
|
242
242
|
|
|
@@ -287,22 +287,22 @@ class GNSSReader:
|
|
|
287
287
|
|
|
288
288
|
return self
|
|
289
289
|
|
|
290
|
-
def __next__(self) -> tuple[bytes
|
|
290
|
+
def __next__(self) -> tuple[bytes, Any]:
|
|
291
291
|
"""
|
|
292
292
|
Return next item in iteration.
|
|
293
293
|
|
|
294
294
|
:return: tuple of (raw_data as bytes, parsed_data as UBXMessage)
|
|
295
|
-
:rtype: tuple[bytes
|
|
295
|
+
:rtype: tuple[bytes, Any]
|
|
296
296
|
:raises: StopIteration
|
|
297
297
|
|
|
298
298
|
"""
|
|
299
299
|
|
|
300
300
|
raw_data, parsed_data = self.read()
|
|
301
|
-
if raw_data
|
|
301
|
+
if raw_data == b"" and parsed_data is None:
|
|
302
302
|
raise StopIteration
|
|
303
303
|
return raw_data, parsed_data
|
|
304
304
|
|
|
305
|
-
def read(self) -> tuple[bytes
|
|
305
|
+
def read(self) -> tuple[bytes, Any]:
|
|
306
306
|
"""
|
|
307
307
|
Read a single NMEA, UBX, SBF, QGC, UNI or RTCM3 message from the stream buffer
|
|
308
308
|
and return both raw and parsed data.
|
|
@@ -312,16 +312,16 @@ class GNSSReader:
|
|
|
312
312
|
|
|
313
313
|
:return: tuple of (raw_data as bytes, parsed_data as NMEAMessage, UBXMessage,
|
|
314
314
|
SBFMessage, QGCMessage, UNIMessage, RTCMMessage or (if `parsing=2`) GNSSMessage)
|
|
315
|
-
:rtype: tuple[bytes
|
|
315
|
+
:rtype: tuple[bytes, Any]
|
|
316
316
|
:raises: Exception (if invalid or unrecognised protocol in data stream)
|
|
317
317
|
"""
|
|
318
318
|
|
|
319
|
-
raw_data =
|
|
319
|
+
raw_data = b""
|
|
320
320
|
parsed_data = None
|
|
321
321
|
while True: # loop until end of valid message or EOF
|
|
322
322
|
try:
|
|
323
323
|
|
|
324
|
-
raw_data =
|
|
324
|
+
# raw_data = b""
|
|
325
325
|
parsed_data = None
|
|
326
326
|
byte1 = self._read_bytes(1) # read the first byte
|
|
327
327
|
# if not UBX, SBF, QGC, UNI, NMEA or RTCM3, discard and continue
|
|
@@ -382,10 +382,11 @@ class GNSSReader:
|
|
|
382
382
|
if self._protfilter & RTCM3_PROTOCOL:
|
|
383
383
|
break
|
|
384
384
|
continue
|
|
385
|
-
|
|
385
|
+
raw_data = bytehdr
|
|
386
|
+
raise GNSSStreamError(f"Unknown protocol header {escapeall(bytehdr)}.")
|
|
386
387
|
|
|
387
388
|
except EOFError:
|
|
388
|
-
return (
|
|
389
|
+
return (b"", None)
|
|
389
390
|
except (
|
|
390
391
|
UBXMessageError,
|
|
391
392
|
UBXTypeError,
|
|
@@ -674,17 +675,17 @@ class GNSSReader:
|
|
|
674
675
|
# pass to error handler if there is one
|
|
675
676
|
# else just log
|
|
676
677
|
if self._errorhandler is None:
|
|
677
|
-
self._logger.
|
|
678
|
+
self._logger.exception(err)
|
|
678
679
|
else:
|
|
679
680
|
self._errorhandler(err)
|
|
680
681
|
|
|
681
682
|
@property
|
|
682
|
-
def datastream(self) ->
|
|
683
|
+
def datastream(self) -> Any:
|
|
683
684
|
"""
|
|
684
685
|
Getter for stream.
|
|
685
686
|
|
|
686
687
|
:return: data stream
|
|
687
|
-
:rtype:
|
|
688
|
+
:rtype: Any
|
|
688
689
|
"""
|
|
689
690
|
|
|
690
691
|
return self._stream
|
|
@@ -253,7 +253,7 @@ def get_mp_distance(lat: float, lon: float, mp: list) -> float:
|
|
|
253
253
|
|
|
254
254
|
def find_mp_distance(
|
|
255
255
|
lat: float, lon: float, sourcetable: list, name: str = ""
|
|
256
|
-
) -> tuple:
|
|
256
|
+
) -> tuple[str, float]:
|
|
257
257
|
"""
|
|
258
258
|
Find distance to named mountpoint. If mountpoint name
|
|
259
259
|
is not provided, find closest mountpoint in sourcetable.
|
|
@@ -269,11 +269,11 @@ def find_mp_distance(
|
|
|
269
269
|
:param list sourcetable: sourcetable as list
|
|
270
270
|
:param str name: (optional) mountpoint name (None)
|
|
271
271
|
:returns: tuple of (name of closest mountpoint, distance in km)
|
|
272
|
-
:rtype: tuple
|
|
272
|
+
:rtype: tuple[str, float]
|
|
273
273
|
"""
|
|
274
274
|
|
|
275
275
|
mindist = 9999999
|
|
276
|
-
mpname =
|
|
276
|
+
mpname = ""
|
|
277
277
|
for mp in sourcetable:
|
|
278
278
|
dist = get_mp_distance(lat, lon, mp)
|
|
279
279
|
if dist is not None:
|
|
@@ -56,7 +56,7 @@ class RinexConverterMeteorology:
|
|
|
56
56
|
verbosity: Literal[-1, 0, 1, 2, 3] = VERBOSITY_MEDIUM,
|
|
57
57
|
logtofile: str = "",
|
|
58
58
|
**kwargs,
|
|
59
|
-
): # pylint: disable=too-many-arguments, too-many-positional-arguments
|
|
59
|
+
): # pylint: disable=too-many-arguments, too-many-positional-arguments, unused-argument
|
|
60
60
|
"""
|
|
61
61
|
Constructor.
|
|
62
62
|
|
|
@@ -111,12 +111,12 @@ class RinexConverterMeteorology:
|
|
|
111
111
|
ret = 0
|
|
112
112
|
if isinstance(parsed, NMEAMessage):
|
|
113
113
|
self._datasource = NMEA
|
|
114
|
-
if parsed.identity[2:]
|
|
114
|
+
if parsed.identity[2:] == "RMC":
|
|
115
115
|
self.get_nmea_epoch(parsed)
|
|
116
|
-
if parsed.identity[2:]
|
|
116
|
+
if parsed.identity[2:] == "MWD": # NMEA Wind Speed and Direction
|
|
117
117
|
self.convert_nmea_mwd(parsed)
|
|
118
118
|
ret += 1
|
|
119
|
-
if parsed.identity[2:]
|
|
119
|
+
if parsed.identity[2:] == "XDR": # NMEA Transducer (Temp & Pressure)
|
|
120
120
|
self.convert_nmea_xdr(parsed)
|
|
121
121
|
ret += 1
|
|
122
122
|
# ADD ADDITIONAL PARSED DATA SOURCES HERE...
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pygnssutils
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.3.0
|
|
4
4
|
Summary: GNSS Command Line Utilities
|
|
5
5
|
Author: Steve Smith
|
|
6
6
|
Author-email: semuadmin@semuconsulting.com
|
|
@@ -34,15 +34,15 @@ Requires-Python: >=3.10
|
|
|
34
34
|
Description-Content-Type: text/markdown
|
|
35
35
|
License-File: LICENSE
|
|
36
36
|
Requires-Dist: certifi>=2026.0.0
|
|
37
|
-
Requires-Dist: pynmeagps>=1.1.
|
|
38
|
-
Requires-Dist: pyqgc>=1.0.
|
|
39
|
-
Requires-Dist: pyrtcm>=1.2.
|
|
40
|
-
Requires-Dist: pysbf2>=1.0.
|
|
37
|
+
Requires-Dist: pynmeagps>=1.1.8
|
|
38
|
+
Requires-Dist: pyqgc>=1.0.1
|
|
39
|
+
Requires-Dist: pyrtcm>=1.2.1
|
|
40
|
+
Requires-Dist: pysbf2>=1.0.7
|
|
41
41
|
Requires-Dist: pyserial>=3.5
|
|
42
|
-
Requires-Dist: pyspartn>=1.0
|
|
43
|
-
Requires-Dist: pyubx2>=1.3.
|
|
42
|
+
Requires-Dist: pyspartn>=1.1.0
|
|
43
|
+
Requires-Dist: pyubx2>=1.3.9
|
|
44
44
|
Requires-Dist: pyubxutils>=1.0.6
|
|
45
|
-
Requires-Dist: pyunigps>=1.0.
|
|
45
|
+
Requires-Dist: pyunigps>=1.0.1
|
|
46
46
|
Dynamic: license-file
|
|
47
47
|
|
|
48
48
|
pygnssutils
|
|
@@ -115,14 +115,14 @@ class StaticTest(unittest.TestCase):
|
|
|
115
115
|
lon = -2.24
|
|
116
116
|
name = "AUADL"
|
|
117
117
|
res = find_mp_distance(lat, lon, TESTSRT, name)
|
|
118
|
-
self.assertEqual(res, (
|
|
118
|
+
self.assertEqual(res, ("", 9999999))
|
|
119
119
|
|
|
120
120
|
def testfindmpdist4(self): # invalid coords
|
|
121
121
|
name = "apinhal"
|
|
122
122
|
lat = ""
|
|
123
123
|
lon = ""
|
|
124
124
|
res = find_mp_distance(lat, lon, TESTSRT, name)
|
|
125
|
-
self.assertEqual(res, (
|
|
125
|
+
self.assertEqual(res, ("", 9999999))
|
|
126
126
|
|
|
127
127
|
def testgetmpdist1(self): # valid
|
|
128
128
|
mp = TESTSRT[12]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|