pygnssutils 1.2.7__tar.gz → 1.2.9__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.7/src/pygnssutils.egg-info → pygnssutils-1.2.9}/PKG-INFO +17 -11
- {pygnssutils-1.2.7 → pygnssutils-1.2.9}/README.md +5 -3
- {pygnssutils-1.2.7 → pygnssutils-1.2.9}/pyproject.toml +12 -7
- {pygnssutils-1.2.7 → pygnssutils-1.2.9}/src/pygnssutils/_version.py +1 -1
- {pygnssutils-1.2.7 → pygnssutils-1.2.9}/src/pygnssutils/gnssntripclient.py +89 -85
- {pygnssutils-1.2.7 → pygnssutils-1.2.9}/src/pygnssutils/gnssreader.py +25 -33
- {pygnssutils-1.2.7 → pygnssutils-1.2.9}/src/pygnssutils/helpers.py +3 -3
- {pygnssutils-1.2.7 → pygnssutils-1.2.9}/src/pygnssutils/rinex_conv.py +0 -1
- {pygnssutils-1.2.7 → pygnssutils-1.2.9}/src/pygnssutils/rinex_conv_met.py +4 -4
- {pygnssutils-1.2.7 → pygnssutils-1.2.9/src/pygnssutils.egg-info}/PKG-INFO +17 -11
- {pygnssutils-1.2.7 → pygnssutils-1.2.9}/src/pygnssutils.egg-info/requires.txt +6 -5
- {pygnssutils-1.2.7 → pygnssutils-1.2.9}/tests/test_static.py +2 -2
- {pygnssutils-1.2.7 → pygnssutils-1.2.9}/LICENSE +0 -0
- {pygnssutils-1.2.7 → pygnssutils-1.2.9}/setup.cfg +0 -0
- {pygnssutils-1.2.7 → pygnssutils-1.2.9}/src/pygnssutils/__init__.py +0 -0
- {pygnssutils-1.2.7 → pygnssutils-1.2.9}/src/pygnssutils/exceptions.py +0 -0
- {pygnssutils-1.2.7 → pygnssutils-1.2.9}/src/pygnssutils/globals.py +0 -0
- {pygnssutils-1.2.7 → pygnssutils-1.2.9}/src/pygnssutils/gnssntripclient_cli.py +0 -0
- {pygnssutils-1.2.7 → pygnssutils-1.2.9}/src/pygnssutils/gnssserver.py +0 -0
- {pygnssutils-1.2.7 → pygnssutils-1.2.9}/src/pygnssutils/gnssserver_cli.py +0 -0
- {pygnssutils-1.2.7 → pygnssutils-1.2.9}/src/pygnssutils/gnssstreamer.py +0 -0
- {pygnssutils-1.2.7 → pygnssutils-1.2.9}/src/pygnssutils/gnssstreamer_cli.py +0 -0
- {pygnssutils-1.2.7 → pygnssutils-1.2.9}/src/pygnssutils/rawnav.py +0 -0
- {pygnssutils-1.2.7 → pygnssutils-1.2.9}/src/pygnssutils/rawnav_reader.py +0 -0
- {pygnssutils-1.2.7 → pygnssutils-1.2.9}/src/pygnssutils/rawnav_subframes_bds.py +0 -0
- {pygnssutils-1.2.7 → pygnssutils-1.2.9}/src/pygnssutils/rawnav_subframes_gal.py +0 -0
- {pygnssutils-1.2.7 → pygnssutils-1.2.9}/src/pygnssutils/rawnav_subframes_glo.py +0 -0
- {pygnssutils-1.2.7 → pygnssutils-1.2.9}/src/pygnssutils/rawnav_subframes_gps.py +0 -0
- {pygnssutils-1.2.7 → pygnssutils-1.2.9}/src/pygnssutils/rawnav_subframes_irn.py +0 -0
- {pygnssutils-1.2.7 → pygnssutils-1.2.9}/src/pygnssutils/rawnav_subframes_qzs.py +0 -0
- {pygnssutils-1.2.7 → pygnssutils-1.2.9}/src/pygnssutils/rawnav_subframes_sba.py +0 -0
- {pygnssutils-1.2.7 → pygnssutils-1.2.9}/src/pygnssutils/rinex_conv_cli.py +0 -0
- {pygnssutils-1.2.7 → pygnssutils-1.2.9}/src/pygnssutils/rinex_conv_nav.py +0 -0
- {pygnssutils-1.2.7 → pygnssutils-1.2.9}/src/pygnssutils/rinex_conv_obs.py +0 -0
- {pygnssutils-1.2.7 → pygnssutils-1.2.9}/src/pygnssutils/rinex_globals.py +0 -0
- {pygnssutils-1.2.7 → pygnssutils-1.2.9}/src/pygnssutils/rinex_helpers.py +0 -0
- {pygnssutils-1.2.7 → pygnssutils-1.2.9}/src/pygnssutils/socket_server.py +0 -0
- {pygnssutils-1.2.7 → pygnssutils-1.2.9}/src/pygnssutils.egg-info/SOURCES.txt +0 -0
- {pygnssutils-1.2.7 → pygnssutils-1.2.9}/src/pygnssutils.egg-info/dependency_links.txt +0 -0
- {pygnssutils-1.2.7 → pygnssutils-1.2.9}/src/pygnssutils.egg-info/entry_points.txt +0 -0
- {pygnssutils-1.2.7 → pygnssutils-1.2.9}/src/pygnssutils.egg-info/top_level.txt +0 -0
- {pygnssutils-1.2.7 → pygnssutils-1.2.9}/tests/test_cli.py +0 -0
- {pygnssutils-1.2.7 → pygnssutils-1.2.9}/tests/test_gnssstreamer.py +0 -0
- {pygnssutils-1.2.7 → pygnssutils-1.2.9}/tests/test_rawnav.py +0 -0
- {pygnssutils-1.2.7 → pygnssutils-1.2.9}/tests/test_rinex.py +0 -0
- {pygnssutils-1.2.7 → pygnssutils-1.2.9}/tests/test_rinex_defs.py +0 -0
- {pygnssutils-1.2.7 → pygnssutils-1.2.9}/tests/test_rinex_parse.py +0 -0
- {pygnssutils-1.2.7 → pygnssutils-1.2.9}/tests/test_socketwrapper.py +0 -0
- {pygnssutils-1.2.7 → pygnssutils-1.2.9}/tests/test_sourcetable.py +0 -0
- {pygnssutils-1.2.7 → pygnssutils-1.2.9}/tests/test_stream.py +0 -0
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pygnssutils
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.9
|
|
4
4
|
Summary: GNSS Command Line Utilities
|
|
5
|
-
Author
|
|
6
|
-
|
|
5
|
+
Author: Steve Smith
|
|
6
|
+
Author-email: semuadmin@semuconsulting.com
|
|
7
|
+
Maintainer: Steve Smith
|
|
8
|
+
Maintainer-email: semuadmin@semuconsulting.com
|
|
7
9
|
License-Expression: BSD-3-Clause
|
|
8
10
|
Project-URL: homepage, https://github.com/semuconsulting/pygnssutils
|
|
9
11
|
Project-URL: documentation, https://www.semuconsulting.com/pygnssutils/
|
|
@@ -24,20 +26,22 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
24
26
|
Classifier: Programming Language :: Python :: 3.12
|
|
25
27
|
Classifier: Programming Language :: Python :: 3.13
|
|
26
28
|
Classifier: Programming Language :: Python :: 3.14
|
|
29
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
27
30
|
Classifier: Topic :: Utilities
|
|
28
31
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
29
32
|
Classifier: Topic :: Scientific/Engineering :: GIS
|
|
30
33
|
Requires-Python: >=3.10
|
|
31
34
|
Description-Content-Type: text/markdown
|
|
32
35
|
License-File: LICENSE
|
|
33
|
-
Requires-Dist: certifi>=
|
|
34
|
-
Requires-Dist: pyserial>=3.5
|
|
35
|
-
Requires-Dist: pyspartn>=1.0.8
|
|
36
|
-
Requires-Dist: pyubx2>=1.3.6
|
|
36
|
+
Requires-Dist: certifi>=2026.0.0
|
|
37
37
|
Requires-Dist: pynmeagps>=1.1.7
|
|
38
|
+
Requires-Dist: pyqgc>=1.0.0
|
|
39
|
+
Requires-Dist: pyrtcm>=1.2.0
|
|
38
40
|
Requires-Dist: pysbf2>=1.0.4
|
|
41
|
+
Requires-Dist: pyserial>=3.5
|
|
42
|
+
Requires-Dist: pyspartn>=1.0.8
|
|
43
|
+
Requires-Dist: pyubx2>=1.3.7
|
|
39
44
|
Requires-Dist: pyubxutils>=1.0.6
|
|
40
|
-
Requires-Dist: pyqgc>=1.0.0
|
|
41
45
|
Requires-Dist: pyunigps>=1.0.0
|
|
42
46
|
Dynamic: license-file
|
|
43
47
|
|
|
@@ -92,18 +96,18 @@ The pygnssutils homepage is located at [https://github.com/semuconsulting/pygnss
|
|
|
92
96
|
|
|
93
97
|
Sphinx API Documentation in HTML format is available at [https://www.semuconsulting.com/pygnssutils](https://www.semuconsulting.com/pygnssutils).
|
|
94
98
|
|
|
95
|
-
Contributions **
|
|
99
|
+
Contributions _**from human beings**_ welcome - please refer to [CONTRIBUTING.MD](https://github.com/semuconsulting/pygnssutils/blob/main/CONTRIBUTING.md).
|
|
96
100
|
|
|
97
101
|
[Bug reports](https://github.com/semuconsulting/pygnssutils/blob/main/.github/ISSUE_TEMPLATE/bug_report.md) and [Feature requests](https://github.com/semuconsulting/pygnssutils/blob/main/.github/ISSUE_TEMPLATE/feature_request.md) - please use the templates provided. For general queries and advice, post a message to one of the [pygnssutils Discussions](https://github.com/semuconsulting/pygnssutils/discussions) channels.
|
|
98
102
|
|
|
99
|
-

|
|
100
104
|
|
|
101
105
|
---
|
|
102
106
|
## <a name="installation">Installation</a>
|
|
103
107
|
|
|
104
108
|

|
|
105
109
|
[](https://pypi.org/project/pygnssutils/)
|
|
106
|
-
[](https://clickpy.clickhouse.com/dashboard/pygnssutils)
|
|
107
111
|
|
|
108
112
|
`pygnssutils` is compatible with Python>=3.10.
|
|
109
113
|
|
|
@@ -630,6 +634,8 @@ semuadmin@semuconsulting.com
|
|
|
630
634
|
|
|
631
635
|

|
|
632
636
|
|
|
637
|
+
[ https://orcid.org/0009-0002-5751-8719](https://orcid.org/0009-0002-5751-8719)
|
|
638
|
+
|
|
633
639
|
`pygnssutils` is maintained entirely by unpaid volunteers. It receives no funding from advertising or corporate sponsorship. If you find the utility useful, please consider sponsoring the project with the price of a coffee...
|
|
634
640
|
|
|
635
641
|
[](https://buymeacoffee.com/semuconsulting)
|
|
@@ -49,18 +49,18 @@ The pygnssutils homepage is located at [https://github.com/semuconsulting/pygnss
|
|
|
49
49
|
|
|
50
50
|
Sphinx API Documentation in HTML format is available at [https://www.semuconsulting.com/pygnssutils](https://www.semuconsulting.com/pygnssutils).
|
|
51
51
|
|
|
52
|
-
Contributions **
|
|
52
|
+
Contributions _**from human beings**_ welcome - please refer to [CONTRIBUTING.MD](https://github.com/semuconsulting/pygnssutils/blob/main/CONTRIBUTING.md).
|
|
53
53
|
|
|
54
54
|
[Bug reports](https://github.com/semuconsulting/pygnssutils/blob/main/.github/ISSUE_TEMPLATE/bug_report.md) and [Feature requests](https://github.com/semuconsulting/pygnssutils/blob/main/.github/ISSUE_TEMPLATE/feature_request.md) - please use the templates provided. For general queries and advice, post a message to one of the [pygnssutils Discussions](https://github.com/semuconsulting/pygnssutils/discussions) channels.
|
|
55
55
|
|
|
56
|
-

|
|
57
57
|
|
|
58
58
|
---
|
|
59
59
|
## <a name="installation">Installation</a>
|
|
60
60
|
|
|
61
61
|

|
|
62
62
|
[](https://pypi.org/project/pygnssutils/)
|
|
63
|
-
[](https://clickpy.clickhouse.com/dashboard/pygnssutils)
|
|
64
64
|
|
|
65
65
|
`pygnssutils` is compatible with Python>=3.10.
|
|
66
66
|
|
|
@@ -587,6 +587,8 @@ semuadmin@semuconsulting.com
|
|
|
587
587
|
|
|
588
588
|

|
|
589
589
|
|
|
590
|
+
[ https://orcid.org/0009-0002-5751-8719](https://orcid.org/0009-0002-5751-8719)
|
|
591
|
+
|
|
590
592
|
`pygnssutils` is maintained entirely by unpaid volunteers. It receives no funding from advertising or corporate sponsorship. If you find the utility useful, please consider sponsoring the project with the price of a coffee...
|
|
591
593
|
|
|
592
594
|
[](https://buymeacoffee.com/semuconsulting)
|
|
@@ -5,8 +5,11 @@ build-backend = "setuptools.build_meta:__legacy__"
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "pygnssutils"
|
|
7
7
|
dynamic = ["version"]
|
|
8
|
-
authors = [{ name = "Steve Smith", email = "semuadmin@semuconsulting.com" }]
|
|
9
|
-
maintainers = [
|
|
8
|
+
authors = [{ name = "Steve Smith" }, { email = "semuadmin@semuconsulting.com" }]
|
|
9
|
+
maintainers = [
|
|
10
|
+
{ name = "Steve Smith" },
|
|
11
|
+
{ email = "semuadmin@semuconsulting.com" },
|
|
12
|
+
]
|
|
10
13
|
description = "GNSS Command Line Utilities"
|
|
11
14
|
license = "BSD-3-Clause"
|
|
12
15
|
license-files = ["LICENSE"]
|
|
@@ -28,20 +31,22 @@ classifiers = [
|
|
|
28
31
|
"Programming Language :: Python :: 3.12",
|
|
29
32
|
"Programming Language :: Python :: 3.13",
|
|
30
33
|
"Programming Language :: Python :: 3.14",
|
|
34
|
+
"Programming Language :: Python :: 3.15",
|
|
31
35
|
"Topic :: Utilities",
|
|
32
36
|
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
33
37
|
"Topic :: Scientific/Engineering :: GIS",
|
|
34
38
|
]
|
|
35
39
|
|
|
36
40
|
dependencies = [
|
|
37
|
-
"certifi>=
|
|
38
|
-
"pyserial>=3.5",
|
|
39
|
-
"pyspartn>=1.0.8",
|
|
40
|
-
"pyubx2>=1.3.6",
|
|
41
|
+
"certifi>=2026.0.0",
|
|
41
42
|
"pynmeagps>=1.1.7",
|
|
43
|
+
"pyqgc>=1.0.0",
|
|
44
|
+
"pyrtcm>=1.2.0",
|
|
42
45
|
"pysbf2>=1.0.4",
|
|
46
|
+
"pyserial>=3.5",
|
|
47
|
+
"pyspartn>=1.0.8",
|
|
48
|
+
"pyubx2>=1.3.7",
|
|
43
49
|
"pyubxutils>=1.0.6",
|
|
44
|
-
"pyqgc>=1.0.0",
|
|
45
50
|
"pyunigps>=1.0.0",
|
|
46
51
|
]
|
|
47
52
|
|
|
@@ -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,28 @@ 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
|
|
|
701
703
|
if output is not None:
|
|
702
704
|
# serialize sourcetable if outputting to stream
|
|
703
705
|
if isinstance(raw, list) and not isinstance(output, Queue):
|
|
704
706
|
raw = self._serialize_sourcetable(raw)
|
|
705
|
-
if isinstance(output, (Serial, BufferedWriter)):
|
|
707
|
+
if isinstance(output, (Serial, BufferedWriter)) and raw is not None:
|
|
706
708
|
output.write(raw)
|
|
707
|
-
elif isinstance(output, TextIOWrapper):
|
|
709
|
+
elif isinstance(output, TextIOWrapper) and parsed is not None:
|
|
708
710
|
output.write(str(parsed))
|
|
709
|
-
elif isinstance(output, Queue):
|
|
711
|
+
elif isinstance(output, Queue) and raw is not None:
|
|
710
712
|
output.put(raw if self.__app == CLIAPP else (raw, parsed))
|
|
711
|
-
elif isinstance(output, socket
|
|
713
|
+
elif isinstance(output, socket) and raw is not None:
|
|
712
714
|
output.sendall(raw)
|
|
713
715
|
|
|
714
716
|
# notify any calling app that data is available
|
|
@@ -716,18 +718,18 @@ class GNSSNTRIPClient:
|
|
|
716
718
|
if hasattr(self.__app, "set_event"):
|
|
717
719
|
self.__app.set_event(NTRIP_EVENT)
|
|
718
720
|
|
|
719
|
-
def _app_update_status(self, status: bool, msgt: tuple
|
|
721
|
+
def _app_update_status(self, status: bool, msgt: tuple[str, str]):
|
|
720
722
|
"""
|
|
721
723
|
Update NTRIP connection status in calling application.
|
|
722
724
|
|
|
723
725
|
:param bool status: NTRIP server connection status
|
|
724
|
-
:param tuple msgt: (message, color)
|
|
726
|
+
:param tuple[str,str] msgt: (message, color)
|
|
725
727
|
"""
|
|
726
728
|
|
|
727
729
|
if msgt[1] == ERRCOL:
|
|
728
730
|
self.logger.error(msgt[0])
|
|
729
731
|
else:
|
|
730
|
-
self.logger.
|
|
732
|
+
self.logger.debug(msgt[0])
|
|
731
733
|
if self.__app is not None:
|
|
732
734
|
if hasattr(self.__app, "dialog"):
|
|
733
735
|
dlg = self.__app.dialog(DLGTNTRIP)
|
|
@@ -866,15 +868,17 @@ class GNSSNTRIPClient:
|
|
|
866
868
|
return self._response_headers.get("content-type", "")
|
|
867
869
|
|
|
868
870
|
@property
|
|
869
|
-
def response_body(self) ->
|
|
871
|
+
def response_body(self) -> bytes | str | NoneType:
|
|
870
872
|
"""
|
|
871
873
|
Get response body if available.
|
|
872
874
|
|
|
873
875
|
:returns: response body as bytes or string, depending on encoding
|
|
874
|
-
:rtype:
|
|
876
|
+
:rtype: bytes | str | NoneType
|
|
875
877
|
"""
|
|
876
878
|
|
|
877
|
-
if
|
|
879
|
+
if isinstance(self._response_body, bytes) and (
|
|
880
|
+
"text/" in self.content_type or self.is_sourcetable
|
|
881
|
+
):
|
|
878
882
|
return self._response_body.decode()
|
|
879
883
|
return self._response_body
|
|
880
884
|
|
|
@@ -13,7 +13,7 @@ Returns both the raw binary data (as bytes) and the parsed data.
|
|
|
13
13
|
|
|
14
14
|
- 'protfilter' governs which protocols (NMEA, UBX, SBF, QGC, UNI or RTCM3) are processed.
|
|
15
15
|
- 'msgfilter' governs which individual message types are processed.
|
|
16
|
-
- `parsing` governs whether payloads are
|
|
16
|
+
- `parsing` governs whether individual payloads are parsed.
|
|
17
17
|
- 'quitonerror' governs how errors are handled.
|
|
18
18
|
- 'msgmode' indicates the type of UBX datastream (output GET, input SET, query POLL).
|
|
19
19
|
If msgmode is set to SETPOLL, input/query mode will be automatically detected by parser.
|
|
@@ -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
|
|
|
@@ -318,8 +318,7 @@ class GNSSReader:
|
|
|
318
318
|
|
|
319
319
|
raw_data = None
|
|
320
320
|
parsed_data = None
|
|
321
|
-
|
|
322
|
-
while parsing: # loop until end of valid message or EOF
|
|
321
|
+
while True: # loop until end of valid message or EOF
|
|
323
322
|
try:
|
|
324
323
|
|
|
325
324
|
raw_data = None
|
|
@@ -336,38 +335,34 @@ class GNSSReader:
|
|
|
336
335
|
# if protocol filter passes UBX, return message,
|
|
337
336
|
# otherwise discard and continue
|
|
338
337
|
if self._protfilter & UBX_PROTOCOL:
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
continue
|
|
338
|
+
break
|
|
339
|
+
continue
|
|
342
340
|
# if it's an NMEA message (b'\x24\x..)
|
|
343
|
-
|
|
341
|
+
if bytehdr in NMEA_HDR:
|
|
344
342
|
raw_data, parsed_data = self._parse_nmea(bytehdr)
|
|
345
343
|
# if protocol filter passes NMEA, return message,
|
|
346
344
|
# otherwise discard and continue
|
|
347
345
|
if self._protfilter & NMEA_PROTOCOL:
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
continue
|
|
346
|
+
break
|
|
347
|
+
continue
|
|
351
348
|
# if it's an SBF message (b'\x24\x40')
|
|
352
|
-
|
|
349
|
+
if bytehdr in SBF_HDR:
|
|
353
350
|
raw_data, parsed_data = self._parse_sbf(bytehdr)
|
|
354
351
|
# if protocol filter passes SBF, return message,
|
|
355
352
|
# otherwise discard and continue
|
|
356
353
|
if self._protfilter & SBF_PROTOCOL:
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
continue
|
|
354
|
+
break
|
|
355
|
+
continue
|
|
360
356
|
# if it's an QGCmessage (b'\x51\x47')
|
|
361
|
-
|
|
357
|
+
if bytehdr in QGC_HDR:
|
|
362
358
|
raw_data, parsed_data = self._parse_qgc(bytehdr)
|
|
363
359
|
# if protocol filter passes QGC, return message,
|
|
364
360
|
# otherwise discard and continue
|
|
365
361
|
if self._protfilter & QGC_PROTOCOL:
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
continue
|
|
362
|
+
break
|
|
363
|
+
continue
|
|
369
364
|
# if it's an UNImessage (b'\xaa\x44\xb5')
|
|
370
|
-
|
|
365
|
+
if bytehdr in UNI_HDR[:2]:
|
|
371
366
|
byte3 = self._read_bytes(1)
|
|
372
367
|
bytehdr += byte3
|
|
373
368
|
if bytehdr != UNI_HDR:
|
|
@@ -376,21 +371,18 @@ class GNSSReader:
|
|
|
376
371
|
# if protocol filter passes UNI, return message,
|
|
377
372
|
# otherwise discard and continue
|
|
378
373
|
if self._protfilter & UNI_PROTOCOL:
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
continue
|
|
374
|
+
break
|
|
375
|
+
continue
|
|
382
376
|
# if it's a RTCM3 message
|
|
383
377
|
# (byte1 = 0xd3; byte2 = 0b000000**)
|
|
384
|
-
|
|
378
|
+
if byte1 == b"\xd3" and (byte2[0] & ~0x03) == 0:
|
|
385
379
|
raw_data, parsed_data = self._parse_rtcm3(bytehdr)
|
|
386
380
|
# if protocol filter passes RTCM, return message,
|
|
387
381
|
# otherwise discard and continue
|
|
388
382
|
if self._protfilter & RTCM3_PROTOCOL:
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
else:
|
|
393
|
-
raise GNSSStreamError(f"Unknown protocol header {bytehdr}.")
|
|
383
|
+
break
|
|
384
|
+
continue
|
|
385
|
+
raise GNSSStreamError(f"Unknown protocol header {escapeall(bytehdr)}.")
|
|
394
386
|
|
|
395
387
|
except EOFError:
|
|
396
388
|
return (None, None)
|
|
@@ -682,17 +674,17 @@ class GNSSReader:
|
|
|
682
674
|
# pass to error handler if there is one
|
|
683
675
|
# else just log
|
|
684
676
|
if self._errorhandler is None:
|
|
685
|
-
self._logger.
|
|
677
|
+
self._logger.exception(err)
|
|
686
678
|
else:
|
|
687
679
|
self._errorhandler(err)
|
|
688
680
|
|
|
689
681
|
@property
|
|
690
|
-
def datastream(self) ->
|
|
682
|
+
def datastream(self) -> Any:
|
|
691
683
|
"""
|
|
692
684
|
Getter for stream.
|
|
693
685
|
|
|
694
686
|
:return: data stream
|
|
695
|
-
:rtype:
|
|
687
|
+
:rtype: Any
|
|
696
688
|
"""
|
|
697
689
|
|
|
698
690
|
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,9 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pygnssutils
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.9
|
|
4
4
|
Summary: GNSS Command Line Utilities
|
|
5
|
-
Author
|
|
6
|
-
|
|
5
|
+
Author: Steve Smith
|
|
6
|
+
Author-email: semuadmin@semuconsulting.com
|
|
7
|
+
Maintainer: Steve Smith
|
|
8
|
+
Maintainer-email: semuadmin@semuconsulting.com
|
|
7
9
|
License-Expression: BSD-3-Clause
|
|
8
10
|
Project-URL: homepage, https://github.com/semuconsulting/pygnssutils
|
|
9
11
|
Project-URL: documentation, https://www.semuconsulting.com/pygnssutils/
|
|
@@ -24,20 +26,22 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
24
26
|
Classifier: Programming Language :: Python :: 3.12
|
|
25
27
|
Classifier: Programming Language :: Python :: 3.13
|
|
26
28
|
Classifier: Programming Language :: Python :: 3.14
|
|
29
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
27
30
|
Classifier: Topic :: Utilities
|
|
28
31
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
29
32
|
Classifier: Topic :: Scientific/Engineering :: GIS
|
|
30
33
|
Requires-Python: >=3.10
|
|
31
34
|
Description-Content-Type: text/markdown
|
|
32
35
|
License-File: LICENSE
|
|
33
|
-
Requires-Dist: certifi>=
|
|
34
|
-
Requires-Dist: pyserial>=3.5
|
|
35
|
-
Requires-Dist: pyspartn>=1.0.8
|
|
36
|
-
Requires-Dist: pyubx2>=1.3.6
|
|
36
|
+
Requires-Dist: certifi>=2026.0.0
|
|
37
37
|
Requires-Dist: pynmeagps>=1.1.7
|
|
38
|
+
Requires-Dist: pyqgc>=1.0.0
|
|
39
|
+
Requires-Dist: pyrtcm>=1.2.0
|
|
38
40
|
Requires-Dist: pysbf2>=1.0.4
|
|
41
|
+
Requires-Dist: pyserial>=3.5
|
|
42
|
+
Requires-Dist: pyspartn>=1.0.8
|
|
43
|
+
Requires-Dist: pyubx2>=1.3.7
|
|
39
44
|
Requires-Dist: pyubxutils>=1.0.6
|
|
40
|
-
Requires-Dist: pyqgc>=1.0.0
|
|
41
45
|
Requires-Dist: pyunigps>=1.0.0
|
|
42
46
|
Dynamic: license-file
|
|
43
47
|
|
|
@@ -92,18 +96,18 @@ The pygnssutils homepage is located at [https://github.com/semuconsulting/pygnss
|
|
|
92
96
|
|
|
93
97
|
Sphinx API Documentation in HTML format is available at [https://www.semuconsulting.com/pygnssutils](https://www.semuconsulting.com/pygnssutils).
|
|
94
98
|
|
|
95
|
-
Contributions **
|
|
99
|
+
Contributions _**from human beings**_ welcome - please refer to [CONTRIBUTING.MD](https://github.com/semuconsulting/pygnssutils/blob/main/CONTRIBUTING.md).
|
|
96
100
|
|
|
97
101
|
[Bug reports](https://github.com/semuconsulting/pygnssutils/blob/main/.github/ISSUE_TEMPLATE/bug_report.md) and [Feature requests](https://github.com/semuconsulting/pygnssutils/blob/main/.github/ISSUE_TEMPLATE/feature_request.md) - please use the templates provided. For general queries and advice, post a message to one of the [pygnssutils Discussions](https://github.com/semuconsulting/pygnssutils/discussions) channels.
|
|
98
102
|
|
|
99
|
-

|
|
100
104
|
|
|
101
105
|
---
|
|
102
106
|
## <a name="installation">Installation</a>
|
|
103
107
|
|
|
104
108
|

|
|
105
109
|
[](https://pypi.org/project/pygnssutils/)
|
|
106
|
-
[](https://clickpy.clickhouse.com/dashboard/pygnssutils)
|
|
107
111
|
|
|
108
112
|
`pygnssutils` is compatible with Python>=3.10.
|
|
109
113
|
|
|
@@ -630,6 +634,8 @@ semuadmin@semuconsulting.com
|
|
|
630
634
|
|
|
631
635
|

|
|
632
636
|
|
|
637
|
+
[ https://orcid.org/0009-0002-5751-8719](https://orcid.org/0009-0002-5751-8719)
|
|
638
|
+
|
|
633
639
|
`pygnssutils` is maintained entirely by unpaid volunteers. It receives no funding from advertising or corporate sponsorship. If you find the utility useful, please consider sponsoring the project with the price of a coffee...
|
|
634
640
|
|
|
635
641
|
[](https://buymeacoffee.com/semuconsulting)
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
certifi>=
|
|
2
|
-
pyserial>=3.5
|
|
3
|
-
pyspartn>=1.0.8
|
|
4
|
-
pyubx2>=1.3.6
|
|
1
|
+
certifi>=2026.0.0
|
|
5
2
|
pynmeagps>=1.1.7
|
|
3
|
+
pyqgc>=1.0.0
|
|
4
|
+
pyrtcm>=1.2.0
|
|
6
5
|
pysbf2>=1.0.4
|
|
6
|
+
pyserial>=3.5
|
|
7
|
+
pyspartn>=1.0.8
|
|
8
|
+
pyubx2>=1.3.7
|
|
7
9
|
pyubxutils>=1.0.6
|
|
8
|
-
pyqgc>=1.0.0
|
|
9
10
|
pyunigps>=1.0.0
|
|
@@ -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
|