pyspartn


Namepyspartn JSON
Version 0.4.0 PyPI version JSON
download
home_pageNone
SummarySPARTN protocol parser
upload_time2024-05-01 09:04:20
maintainerNone
docs_urlNone
authorNone
requires_python>=3.8
licenseBSD 3-Clause License ("BSD License 2.0", "Revised BSD License", "New BSD License", or "Modified BSD License") Copyright (c) 2023, SEMU Consulting All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the <organization> nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
keywords
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            # pyspartn

[Current Status](#currentstatus) |
[Installation](#installation) |
[Reading](#reading) |
[Parsing](#parsing) |
[Generating](#generating) |
[Serializing](#serializing) |
[Examples](#examples) |
[Troubleshooting](#troubleshooting) |
[Graphical Client](#gui) |
[Author & License](#author)

`pyspartn` is an original Python 3 parser for the SPARTN &copy; GPS/GNSS protocol. SPARTN is an open-source GPS/GNSS [differential correction or DGPS](https://en.wikipedia.org/wiki/Differential_GPS) protocol published by u-blox:

[SPARTN Protocol](https://www.spartnformat.org/download/) (available in the public domain).
© 2021 u-blox AG. All rights reserved.

The `pyspartn` homepage is located at [https://github.com/semuconsulting/pyspartn](https://github.com/semuconsulting/pyspartn).

This is an independent project and we have no affiliation whatsoever with u-blox.

**FYI** There are companion libraries which handle standard NMEA 0183 &copy;, UBX &copy; (u-blox) and RTCM3 &copy; GNSS/GPS messages:
- [pyubx2](http://github.com/semuconsulting/pyubx2)
- [pynmeagps](http://github.com/semuconsulting/pynmeagps)
- [pyrtcm](http://github.com/semuconsulting/pyrtcm)

## <a name="currentstatus">Current Status</a>

**CURRENTLY IN BETA**

![Status](https://img.shields.io/pypi/status/pyspartn)
![Release](https://img.shields.io/github/v/release/semuconsulting/pyspartn?include_prereleases)
![Build](https://img.shields.io/github/actions/workflow/status/semuconsulting/pyspartn/main.yml?branch=main)
![Codecov](https://img.shields.io/codecov/c/github/semuconsulting/pyspartn)
![Release Date](https://img.shields.io/github/release-date-pre/semuconsulting/pyspartn)
![Last Commit](https://img.shields.io/github/last-commit/semuconsulting/pyspartn)
![Contributors](https://img.shields.io/github/contributors/semuconsulting/pyspartn.svg)
![Open Issues](https://img.shields.io/github/issues-raw/semuconsulting/pyspartn)

The `SPARTNReader` class is capable of parsing individual SPARTN transport-layer messages from a binary data stream containing *solely* SPARTN data, with their associated metadata (message type/subtype, payload length, encryption parameters, etc.).

The `SPARTNMessage` class implements optional decrypt and decode algorithms for individual OCB, HPAC, GAD, BPAC and EAS-DYN message types. Test coverage is currently limited by available SPARTN test data sources.

Sphinx API Documentation in HTML format is available at [https://www.semuconsulting.com/pyspartn](https://www.semuconsulting.com/pyspartn).

Contributions welcome - please refer to [CONTRIBUTING.MD](https://github.com/semuconsulting/pyspartn/blob/master/CONTRIBUTING.md).

[Bug reports](https://github.com/semuconsulting/pyspartn/blob/master/.github/ISSUE_TEMPLATE/bug_report.md) and [Feature requests](https://github.com/semuconsulting/pyspartn/blob/master/.github/ISSUE_TEMPLATE/feature_request.md) - please use the templates provided. For general queries and advice, please use the [Discussion](https://github.com/semuconsulting/pyspartn/discussions) Forum.

---
## <a name="installation">Installation</a>

`pyspartn` is compatible with Python >=3.8 and is dependent on the `cryptography` library.

**NB:** If you're installing `pyspartn` on a 32-bit Linux platform, some additional installation steps may be required - see note *¹* below.

In the following, `python3` & `pip` refer to the Python 3 executables. You may need to type 
`python` or `pip3`, depending on your particular environment.

![Python version](https://img.shields.io/pypi/pyversions/pyspartn.svg?style=flat)
[![PyPI version](https://img.shields.io/pypi/v/pyspartn)](https://pypi.org/project/pyspartn/)
![PyPI downloads](https://img.shields.io/pypi/dm/pyspartn.svg?style=flat)

The recommended way to install the latest version of `pyspartn` is with
[pip](http://pypi.python.org/pypi/pip/):

```shell
python3 -m pip install --upgrade pyspartn
```

If required, `pyspartn` can also be installed into a virtual environment, e.g.:

```shell
python3 -m pip install --user --upgrade virtualenv
python3 -m virtualenv env
source env/bin/activate (or env\Scripts\activate on Windows)
(env) python3 -m pip install --upgrade pyspartn
...
deactivate
```

*¹* On some 32-bit Linux platforms (e.g. Raspberry Pi OS 32), it may be necessary to [install Rust compiler support](https://www.rust-lang.org/tools/install) in order to install the `cryptography` library which `pyspartn` depends on to decrypt SPARTN messages (see [Discussion](https://github.com/semuconsulting/PyGPSClient/discussions/83#discussioncomment-6635558)):

See [cryptography install README](https://github.com/semuconsulting/pyspartn/blob/main/cryptography_installation/README.md).


For [Conda](https://docs.conda.io/en/latest/) users, `pyspartn` is also available from [conda-forge](https://github.com/conda-forge/pyspartn-feedstock):

[![Anaconda-Server Badge](https://anaconda.org/conda-forge/pyspartn/badges/version.svg)](https://anaconda.org/conda-forge/pyspartn)
[![Anaconda-Server Badge](https://anaconda.org/conda-forge/pyspartn/badges/downloads.svg)](https://anaconda.org/conda-forge/pyspartn)

```shell
conda install -c conda-forge pyspartn
```

---
## <a name="reading">Reading (Streaming)</a>

```
class pyspartn.spartnreader.SPARTNReader(stream, **kwargs)
```

You can create a `SPARTNReader` object by calling the constructor with an active stream object. 
The stream object can be any data stream which supports a `read(n) -> bytes` method (e.g. File or Serial, with 
or without a buffer wrapper). `pyspartn` implements an internal `SocketStream` class to allow sockets to be read in the same way as other streams (see example below).

Individual SPARTN messages can then be read using the `SPARTNReader.read()` function, which returns both the raw binary data (as bytes) and the parsed data (as a `SPARTNMessage`, via the `parse()` method). The function is thread-safe in so far as the incoming data stream object is thread-safe. `SPARTNReader` also implements an iterator. See examples below.

Example -  Serial input:
```python
from serial import Serial
from pyspartn import SPARTNReader
stream = Serial('/dev/tty.usbmodem14101', 9600, timeout=3)
spr = SPARTNReader(stream)
(raw_data, parsed_data) = spr.read()
print(parsed_data)
```

Example - File input (using iterator).
```python
from pyspartn import SPARTNReader
stream = open('spartndata.log', 'rb')
spr = SPARTNReader(stream)
for (raw_data, parsed_data) in spr:
   print(parsed_data)
```

Example - Socket input (using iterator):
```python
import socket
from pyspartn import SPARTNReader
stream = socket.socket(socket.AF_INET, socket.SOCK_STREAM):
stream.connect(("localhost", 50007))
spr = SPARTNReader(stream)
for (raw_data, parsed_data) in spr:
   print(parsed_data)
```

#### Encrypted Payloads

Some proprietary SPARTN message sources (e.g. Thingstream PointPerfect © MQTT) use encrypted payloads (`eaf=1`). In order to decrypt and decode these payloads, the user must set `decode=1` and provide a valid decryption `key`. Keys are typically 32-character hexadecimal strings valid for a 4 week period. If the datastream contains messages with ambiguous 16-bit gnssTimetags (`timeTagtype=0`) - which generally includes all GAD messages and some OCB messages - a nominal `basedate` is also required, representing the date on which the datastream was originally created to the nearest half day. If you're parsing data in real time, this can be left at the default `datetime.now(timezone.utc)`. If you're parsing historical data, you will need to provide a basedate representing the date on which the datastream was originally created to the nearest half day. See examples below.

The current decryption key can also be set via environment variable `MQTTKEY`, but bear in mind this will need amending every 4 weeks.

Example -  Real time serial input with decryption:
```python
from serial import Serial
from pyspartn import SPARTNReader
stream = Serial('/dev/tty.usbmodem14101', 9600, timeout=3)
spr = SPARTNReader(stream, decode=1, key="930d847b779b126863c8b3b2766ae7cc")
for (raw_data, parsed_data) in spr:
   print(parsed_data)
```

Example - Historical file input with decryption.
```python
from datetime import datetime
from pyspartn import SPARTNReader
stream = open('spartndata.log', 'rb')
spr = SPARTNReader(stream, decode=1, key="930d847b779b126863c8b3b2766ae7cc", basedate=datetime(2023, 4, 18, 20, 48, 29, 977255))
for (raw_data, parsed_data) in spr:
   print(parsed_data)
```

---
## <a name="parsing">Parsing</a>

You can parse individual SPARTN messages using the static `SPARTNReader.parse(data)` function, which takes a bytes array containing a binary SPARTN message and returns a `SPARTNMessage` object. If the message payload is encrypted (`eaf=1`), a decryption `key` and `basedate` must be provided. See examples below.

**NB:** Once instantiated, a `SPARTNMMessage` object is immutable.

Example - without payload decryption or decoding:

```python
from pyspartn import SPARTNReader

transport = b"s\x00\x12\xe2\x00|\x10[\x12H\xf5\t\xa0\xb4+\x99\x02\x15\xe2\x05\x85\xb7\x83\xc5\xfd\x0f\xfe\xdf\x18\xbe\x7fv \xc3`\x82\x98\x10\x07\xdc\xeb\x82\x7f\xcf\xf8\x9e\xa3ta\xad"
msg = SPARTNReader.parse(transport, decode=0)
print(msg)
```
```
<SPARTN(SPARTN-1X-OCB-GPS, msgType=0, nData=37, eaf=1, crcType=2, frameCrc=2, msgSubtype=0, timeTagtype=0, gnssTimeTag=3970, solutionId=5, solutionProcId=11, encryptionId=1, encryptionSeq=9, authInd=1, embAuthLen=0, crc=7627181, )>
```

Example - with payload decryption and decoding (requires key and, for messages where `timeTagtype=0`, a nominal basedate):

```python
from datetime import datetime
from pyspartn import SPARTNReader

transport = b"\x73\x04\x19\x62\x03\xfa\x20\x5b\x1f\xc8\x31\x0b\x03\xd3\xa4\xb1\xdb\x79\x21\xcb\x5c\x27\x12\xa7\xa8\xc2\x52\xfd\x4a\xfb\x1a\x96\x3b\x64\x2a\x4e\xcd\x86\xbb\x31\x7c\x61\xde\xf5\xdb\x3d\xa3\x2c\x65\xd5\x05\x9f\x1c\xd9\x96\x47\x3b\xca\x13\x5e\x5e\x54\x80"
msg = SPARTNReader.parse(
    transport,
    decode=1,
    key="6b30302427df05b4d98911ebff3a4d95",
    basedate=datetime(2023, 6, 27, 22, 3, 0),
)
print(msg)
```
```
<SPARTN(SPARTN-1X-GAD, msgType=2, nData=50, eaf=1, crcType=2, frameCrc=2, msgSubtype=0, timeTagtype=0, gnssTimeTag=32580, solutionId=5, solutionProcId=11, encryptionId=1, encryptionSeq=63, authInd=1, embAuthLen=0, crc=6182016, SF005=37, SF068=1, SF069=0, SF030=7, SF031_01=32, SF032_01=43.20000000000002, SF033_01=18.700000000000017, SF034_01=6, SF035_01=2, SF036_01=0.6, SF037_01=2.3000000000000003, SF031_02=33, SF032_02=43.20000000000002, SF033_02=23.30000000000001, SF034_02=6, SF035_02=3, SF036_02=0.6, SF037_02=1.7000000000000002, SF031_03=34, SF032_03=40.099999999999994, SF033_03=12.100000000000023, SF034_03=2, SF035_03=6, SF036_03=1.9000000000000001, SF037_03=1.1, SF031_04=35, SF032_04=39.70000000000002, SF033_04=18.700000000000017, SF034_04=3, SF035_04=3, SF036_04=1.3000000000000003, SF037_04=2.3000000000000003, SF031_05=36, SF032_05=54.80000000000001, SF033_05=-3.1999999999999886, SF034_05=6, SF035_05=2, SF036_05=0.6, SF037_05=3.1, SF031_06=37, SF032_06=49.099999999999994, SF033_06=-5.5, SF034_06=4, SF035_06=7, SF036_06=0.8, SF037_06=1.1, SF031_07=38, SF032_07=46.0, SF033_07=10.600000000000023, SF034_07=3, SF035_07=2, SF036_07=0.9, SF037_07=2.3000000000000003, SF031_08=39, SF032_08=46.0, SF033_08=1.8000000000000114, SF034_08=7, SF035_08=2, SF036_08=0.7000000000000001, SF037_08=2.3000000000000003)>
```

The `SPARTNMessage` object exposes different public attributes depending on its message type or 'identity'. SPARTN data fields are denoted `SFnnn` - use the `datadesc()` helper method to obtain a more user-friendly text description of the data field.

```python
from datetime import datetime
from pyspartn import SPARTNReader, datadesc
msg = SPARTNReader.parse(b"s\x02\xf7\xeb\x08\xd7!\xef\x80[\x17\x88\xc2?\x0f\x ... \xc4#fFy\xb9\xd5", decode=True, key="930d847b779b126863c8b3b2766ae7cc", basedate=datetime(2024, 4, 18, 20, 48, 29, 977255))
print(msg)
print(msg.identity)
print(msg.gnssTimeTag)
print(datadesc("SF005"), msg.SF005)
print(datadesc("SF061a"), msg.SF061a_10_05)
```
```
<SPARTN(SPARTN-1X-HPAC-GPS, msgType=1, nData=495, eaf=1, crcType=2, frameCrc=11, msgSubtype=0, timeTagtype=1, gnssTimeTag=451165680, solutionId=5, solutionProcId=11, encryptionId=1, encryptionSeq=30, authInd=1, embAuthLen=0, crc=7977429, SF005=152, SF068=1, SF069=0, SF030=9, SF031_01=0, SF039_01=0, SF040T_01=1, SF040I_01=1, SF041_01=1, SF042_01=1, SF043_01=0.0, SF044_01=1, SF048_01=-0.21199999999999997, SF049a_01=0.0, SF049b_01=0.0010000000000000009, SF054_01=1, SatBitmaskLen_01=0, SF011_01=880836738, SF055_01_01=1, SF056_01_01=1, SF060_01_01=-11.120000000000005, ..., SF061a_10_05=-0.27200000000000557, SF061b_10_05=0.1839999999999975, SF055_10_06=2, SF056_10_06=1, SF060_10_06=7.640000000000043, SF061a_10_06=-1.3840000000000003, SF061b_10_06=-0.7920000000000016)>
'SPARTN-1X-HPAC-GPS'
451165680
('Solution issue of update (SIOU)', 152)
('Large ionosphere coefficient C01', -0.27200000000000557)
```

Attributes in nested repeating groups are suffixed with a 2-digit index for each nested level e.g. `SF032_06`, `SF061a_10_05`. To iterate through nested grouped attributes, you can use a construct similar to the following (_this example iterates through SF032 Area reference latitude values in a SPARTN-1X-GAD message_):

```python
vals = []
for i in range(parsed_data.SF030 + 1):  # attribute or formula representing group size
    vals.append(getattr(parsed_data, f"SF032_{i+1:02d}"))
print(vals)
```

Enumerations for coded values can be found in [spartntables.py](https://github.com/semuconsulting/pyspartn/blob/main/src/pyspartn/spartntables.py).

The `payload` attribute always contains the raw payload as bytes.

---
## <a name="generating">Generating</a>

```
class pyspartn.spartnmessage.SPARTNMessage(**kwargs)
```

You can create an `SPARTNMessage` object by calling the constructor with the following keyword arguments:
1. transport as bytes

Example:

```python
from pyspartn import SPARTNMessage
msg = SPARTNMessage(transport=b"s\x00\x12\xe2\x00|\x10[\x12H\xf5\t\xa0\xb4+\x99\x02\x15\xe2\x05\x85\xb7\x83\xc5\xfd\x0f\xfe\xdf\x18\xbe\x7fv \xc3`\x82\x98\x10\x07\xdc\xeb\x82\x7f\xcf\xf8\x9e\xa3ta\xad")
print(msg)
```
```
<SPARTN(SPARTN-1X-OCB-GPS, msgType=0, nData=37, eaf=1, crcType=2, frameCrc=2, msgSubtype=0, timeTagtype=0, gnssTimeTag=3970, solutionId=5, solutionProcId=11, encryptionId=1, encryptionSeq=9, authInd=1, embAuthLen=0, crc=7627181, )>
```

---
## <a name="serializing">Serializing</a>

The `SPARTNMessage` class implements a `serialize()` method to convert a `SPARTNMMessage` object to a bytes array suitable for writing to an output stream.

e.g. to create and send a SPARTN-1X-OCB-GPS message type:

```python
from serial import Serial
serialOut = Serial('/dev/ttyACM1', 38400, timeout=5)
from pyspartn import SPARTNMessage
msg = SPARTNMessage(transport=b"s\x00\x12\xe2\x00|\x10[\x12H\xf5\t\xa0\xb4+\x99\x02\x15\xe2\x05\x85\xb7\x83\xc5\xfd\x0f\xfe\xdf\x18\xbe\x7fv \xc3`\x82\x98\x10\x07\xdc\xeb\x82\x7f\xcf\xf8\x9e\xa3ta\xad")
print(msg)
output = msg.serialize()
print(output)
serialOut.write(output)
```
```
<SPARTN(SPARTN-1X-OCB-GPS, msgType=0, nData=37, eaf=1, crcType=2, frameCrc=2, msgSubtype=0, timeTagtype=0, gnssTimeTag=3970, solutionId=5, solutionProcId=11, encryptionId=1, encryptionSeq=9, authInd=1, embAuthLen=0, crc=7627181, )>
b's\x00\x12\xe2\x00|\x10[\x12H\xf5\t\xa0\xb4+\x99\x02\x15\xe2\x05\x85\xb7\x83\xc5\xfd\x0f\xfe\xdf\x18\xbe\x7fv \xc3`\x82\x98\x10\x07\xdc\xeb\x82\x7f\xcf\xf8\x9e\xa3ta\xad'
```

---
## <a name="examples">Examples</a>

The following examples are available in the /examples folder:

1. `spartn_mqtt_client.py` - implements a simple SPARTN MQTT client using the pygnssutils.GNSSMQTTClient class. **NB**: requires a valid ClientID for a
SPARTN MQTT service e.g. u-blox Thingstream PointPerfect MQTT.
1. `spartn_ntrip_client.py` - implements a simple SPARTN NTRIP client using the pygnssutils.GNSSNTRIPClient class. **NB**: requires a valid user and password for a
SPARTN NTRIP service e.g. u-blox Thingstream PointPerfect NTRIP.
1. `spartn_decrypt.py` - illustrates how to read, decrypt and decode a binary SPARTN log file (e.g. from the `spartn_mqtt_client.py` or `spartn_ntrip_client.py` examples above).
1. `rxmpmp_extract_spartn.py` - ilustrates how to extract individual SPARTN messages from the accumulated UBX-RXM-PMP data output by an NEO-D9S L-band correction receiver.
1. `spartnparser.py` - illustrates how to parse SPARTN transport layer data from the binary SPARTN messages output by the `rxmpmp_extract_spartn.py` above.
1. `gad_plot.py` - illustrates how to extract geographic area definitions from a series of SPARTN-GAD-1X messages - the output file from the example above can be used as an input. This example also serves to illustrate how to decrypt SPARTN messages.

---
## <a name="troubleshooting">Troubleshooting</a>

1. `SPARTNTypeError` or `SPARTNParseError` when parsing encrypted messages with 16-bit gnssTimetags (`timeTagtype=0`), e.g. GAD or some OCB messages:

   ```
   pyspartn.exceptions.SPARTNTypeError: Error processing attribute 'group' in message type SPARTN-1X-GAD
   ```

   This is almost certainly due to an invalid decryption key and/or basedate. Remember that keys are only valid for a 4 week period, and basedates are valid for no more than half a day. Note also that different GNSS constellations use different UTC datums e.g. GLONASS timestamps are based on UTC+3. Check with your SPARTN service provider for the latest decryption key(s), and check the original creation date of your SPARTN datasource.

1. `SSL: CERTIFICATE_VERIFY_FAILED` error when attempting to connect to SPARTN MQTT service using `gnssmqttclient` on MacOS:

   ```
   [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1000)
   ```

   This is because `gnssmqttclient` is unable to locate the RootCA certificate for the MQTT Broker. This can normally be resolved as follows:
   - Install the latest version of certifi: ```python3 -m pip install --upgrade certifi```
   - Run the following command from the terminal (_substituting your Python path and version as required_): ```/Applications/Python\ 3.12/Install\ Certificates.command```

1. Unable to install `crytography` library required by `pyspartn` on 32-bit Linux platforms:

   ```
   Building wheel for cryptography (PEP 517): started
   Building wheel for cryptography (PEP 517): finished with status 'error'
   ```

   Refer to [cryptography installation README.md](https://github.com/semuconsulting/pyspartn/blob/main/cryptography_installation/README.md).

1. Checking for successful decryption. `SPARTNMessage` objects implement a protected attribute `_padding`, which represents the number of redundant bits added to the payload content in order to byte-align the payload with the number of bytes specified in the transport layer payload length attribute `nData`. If the payload has been successfully decrypted and decoded, the value of `_padding` should always be between 0 and 8. Checking `0 <= msg._padding <= 8` provides an informal (_but not necessarily definitive_) check of successful decryption and decoding (see, for example, [spartn_decrypt.py](https://github.com/semuconsulting/pyspartn/blob/main/examples/spartn_decrypt.py)).

---
## <a name="gui">Graphical Client</a>

A python/tkinter graphical GPS client which supports NMEA, UBX, RTCM3 and SPARTN protocols is available at: 

[https://github.com/semuconsulting/PyGPSClient](https://github.com/semuconsulting/PyGPSClient)

---
## <a name="author">Author & License Information</a>

semuadmin@semuconsulting.com

![License](https://img.shields.io/github/license/semuconsulting/pyspartn.svg)

`pyspartn` is maintained entirely by unpaid volunteers. It receives no funding from advertising or corporate sponsorship. If you find the library useful, a small donation would be greatly appreciated!

[![Donations](https://www.paypalobjects.com/en_GB/i/btn/btn_donate_LG.gif)](https://www.paypal.com/donate/?business=UL24WUA4XHNRY&no_recurring=0&item_name=The+SEMU+GNSS+Python+libraries+are+maintained+entirely+by+unpaid+volunteers.+All+donations+are+greatly+appreciated.&currency_code=GBP)

            

Raw data

            {
    "_id": null,
    "home_page": null,
    "name": "pyspartn",
    "maintainer": null,
    "docs_url": null,
    "requires_python": ">=3.8",
    "maintainer_email": "semuadmin <semuadmin@semuconsulting.com>",
    "keywords": null,
    "author": null,
    "author_email": "semuadmin <semuadmin@semuconsulting.com>",
    "download_url": "https://files.pythonhosted.org/packages/72/7b/8b3c801ca8c00942be15f0bfc01481e21844ade47409f79ea5177ca3e56b/pyspartn-0.4.0.tar.gz",
    "platform": null,
    "description": "# pyspartn\n\n[Current Status](#currentstatus) |\n[Installation](#installation) |\n[Reading](#reading) |\n[Parsing](#parsing) |\n[Generating](#generating) |\n[Serializing](#serializing) |\n[Examples](#examples) |\n[Troubleshooting](#troubleshooting) |\n[Graphical Client](#gui) |\n[Author & License](#author)\n\n`pyspartn` is an original Python 3 parser for the SPARTN &copy; GPS/GNSS protocol. SPARTN is an open-source GPS/GNSS [differential correction or DGPS](https://en.wikipedia.org/wiki/Differential_GPS) protocol published by u-blox:\n\n[SPARTN Protocol](https://www.spartnformat.org/download/) (available in the public domain).\n\u00a9 2021 u-blox AG. All rights reserved.\n\nThe `pyspartn` homepage is located at [https://github.com/semuconsulting/pyspartn](https://github.com/semuconsulting/pyspartn).\n\nThis is an independent project and we have no affiliation whatsoever with u-blox.\n\n**FYI** There are companion libraries which handle standard NMEA 0183 &copy;, UBX &copy; (u-blox) and RTCM3 &copy; GNSS/GPS messages:\n- [pyubx2](http://github.com/semuconsulting/pyubx2)\n- [pynmeagps](http://github.com/semuconsulting/pynmeagps)\n- [pyrtcm](http://github.com/semuconsulting/pyrtcm)\n\n## <a name=\"currentstatus\">Current Status</a>\n\n**CURRENTLY IN BETA**\n\n![Status](https://img.shields.io/pypi/status/pyspartn)\n![Release](https://img.shields.io/github/v/release/semuconsulting/pyspartn?include_prereleases)\n![Build](https://img.shields.io/github/actions/workflow/status/semuconsulting/pyspartn/main.yml?branch=main)\n![Codecov](https://img.shields.io/codecov/c/github/semuconsulting/pyspartn)\n![Release Date](https://img.shields.io/github/release-date-pre/semuconsulting/pyspartn)\n![Last Commit](https://img.shields.io/github/last-commit/semuconsulting/pyspartn)\n![Contributors](https://img.shields.io/github/contributors/semuconsulting/pyspartn.svg)\n![Open Issues](https://img.shields.io/github/issues-raw/semuconsulting/pyspartn)\n\nThe `SPARTNReader` class is capable of parsing individual SPARTN transport-layer messages from a binary data stream containing *solely* SPARTN data, with their associated metadata (message type/subtype, payload length, encryption parameters, etc.).\n\nThe `SPARTNMessage` class implements optional decrypt and decode algorithms for individual OCB, HPAC, GAD, BPAC and EAS-DYN message types. Test coverage is currently limited by available SPARTN test data sources.\n\nSphinx API Documentation in HTML format is available at [https://www.semuconsulting.com/pyspartn](https://www.semuconsulting.com/pyspartn).\n\nContributions welcome - please refer to [CONTRIBUTING.MD](https://github.com/semuconsulting/pyspartn/blob/master/CONTRIBUTING.md).\n\n[Bug reports](https://github.com/semuconsulting/pyspartn/blob/master/.github/ISSUE_TEMPLATE/bug_report.md) and [Feature requests](https://github.com/semuconsulting/pyspartn/blob/master/.github/ISSUE_TEMPLATE/feature_request.md) - please use the templates provided. For general queries and advice, please use the [Discussion](https://github.com/semuconsulting/pyspartn/discussions) Forum.\n\n---\n## <a name=\"installation\">Installation</a>\n\n`pyspartn` is compatible with Python >=3.8 and is dependent on the `cryptography` library.\n\n**NB:** If you're installing `pyspartn` on a 32-bit Linux platform, some additional installation steps may be required - see note *\u00b9* below.\n\nIn the following, `python3` & `pip` refer to the Python 3 executables. You may need to type \n`python` or `pip3`, depending on your particular environment.\n\n![Python version](https://img.shields.io/pypi/pyversions/pyspartn.svg?style=flat)\n[![PyPI version](https://img.shields.io/pypi/v/pyspartn)](https://pypi.org/project/pyspartn/)\n![PyPI downloads](https://img.shields.io/pypi/dm/pyspartn.svg?style=flat)\n\nThe recommended way to install the latest version of `pyspartn` is with\n[pip](http://pypi.python.org/pypi/pip/):\n\n```shell\npython3 -m pip install --upgrade pyspartn\n```\n\nIf required, `pyspartn` can also be installed into a virtual environment, e.g.:\n\n```shell\npython3 -m pip install --user --upgrade virtualenv\npython3 -m virtualenv env\nsource env/bin/activate (or env\\Scripts\\activate on Windows)\n(env) python3 -m pip install --upgrade pyspartn\n...\ndeactivate\n```\n\n*\u00b9* On some 32-bit Linux platforms (e.g. Raspberry Pi OS 32), it may be necessary to [install Rust compiler support](https://www.rust-lang.org/tools/install) in order to install the `cryptography` library which `pyspartn` depends on to decrypt SPARTN messages (see [Discussion](https://github.com/semuconsulting/PyGPSClient/discussions/83#discussioncomment-6635558)):\n\nSee [cryptography install README](https://github.com/semuconsulting/pyspartn/blob/main/cryptography_installation/README.md).\n\n\nFor [Conda](https://docs.conda.io/en/latest/) users, `pyspartn` is also available from [conda-forge](https://github.com/conda-forge/pyspartn-feedstock):\n\n[![Anaconda-Server Badge](https://anaconda.org/conda-forge/pyspartn/badges/version.svg)](https://anaconda.org/conda-forge/pyspartn)\n[![Anaconda-Server Badge](https://anaconda.org/conda-forge/pyspartn/badges/downloads.svg)](https://anaconda.org/conda-forge/pyspartn)\n\n```shell\nconda install -c conda-forge pyspartn\n```\n\n---\n## <a name=\"reading\">Reading (Streaming)</a>\n\n```\nclass pyspartn.spartnreader.SPARTNReader(stream, **kwargs)\n```\n\nYou can create a `SPARTNReader` object by calling the constructor with an active stream object. \nThe stream object can be any data stream which supports a `read(n) -> bytes` method (e.g. File or Serial, with \nor without a buffer wrapper). `pyspartn` implements an internal `SocketStream` class to allow sockets to be read in the same way as other streams (see example below).\n\nIndividual SPARTN messages can then be read using the `SPARTNReader.read()` function, which returns both the raw binary data (as bytes) and the parsed data (as a `SPARTNMessage`, via the `parse()` method). The function is thread-safe in so far as the incoming data stream object is thread-safe. `SPARTNReader` also implements an iterator. See examples below.\n\nExample -  Serial input:\n```python\nfrom serial import Serial\nfrom pyspartn import SPARTNReader\nstream = Serial('/dev/tty.usbmodem14101', 9600, timeout=3)\nspr = SPARTNReader(stream)\n(raw_data, parsed_data) = spr.read()\nprint(parsed_data)\n```\n\nExample - File input (using iterator).\n```python\nfrom pyspartn import SPARTNReader\nstream = open('spartndata.log', 'rb')\nspr = SPARTNReader(stream)\nfor (raw_data, parsed_data) in spr:\n   print(parsed_data)\n```\n\nExample - Socket input (using iterator):\n```python\nimport socket\nfrom pyspartn import SPARTNReader\nstream = socket.socket(socket.AF_INET, socket.SOCK_STREAM):\nstream.connect((\"localhost\", 50007))\nspr = SPARTNReader(stream)\nfor (raw_data, parsed_data) in spr:\n   print(parsed_data)\n```\n\n#### Encrypted Payloads\n\nSome proprietary SPARTN message sources (e.g. Thingstream PointPerfect \u00a9 MQTT) use encrypted payloads (`eaf=1`). In order to decrypt and decode these payloads, the user must set `decode=1` and provide a valid decryption `key`. Keys are typically 32-character hexadecimal strings valid for a 4 week period. If the datastream contains messages with ambiguous 16-bit gnssTimetags (`timeTagtype=0`) - which generally includes all GAD messages and some OCB messages - a nominal `basedate` is also required, representing the date on which the datastream was originally created to the nearest half day. If you're parsing data in real time, this can be left at the default `datetime.now(timezone.utc)`. If you're parsing historical data, you will need to provide a basedate representing the date on which the datastream was originally created to the nearest half day. See examples below.\n\nThe current decryption key can also be set via environment variable `MQTTKEY`, but bear in mind this will need amending every 4 weeks.\n\nExample -  Real time serial input with decryption:\n```python\nfrom serial import Serial\nfrom pyspartn import SPARTNReader\nstream = Serial('/dev/tty.usbmodem14101', 9600, timeout=3)\nspr = SPARTNReader(stream, decode=1, key=\"930d847b779b126863c8b3b2766ae7cc\")\nfor (raw_data, parsed_data) in spr:\n   print(parsed_data)\n```\n\nExample - Historical file input with decryption.\n```python\nfrom datetime import datetime\nfrom pyspartn import SPARTNReader\nstream = open('spartndata.log', 'rb')\nspr = SPARTNReader(stream, decode=1, key=\"930d847b779b126863c8b3b2766ae7cc\", basedate=datetime(2023, 4, 18, 20, 48, 29, 977255))\nfor (raw_data, parsed_data) in spr:\n   print(parsed_data)\n```\n\n---\n## <a name=\"parsing\">Parsing</a>\n\nYou can parse individual SPARTN messages using the static `SPARTNReader.parse(data)` function, which takes a bytes array containing a binary SPARTN message and returns a `SPARTNMessage` object. If the message payload is encrypted (`eaf=1`), a decryption `key` and `basedate` must be provided. See examples below.\n\n**NB:** Once instantiated, a `SPARTNMMessage` object is immutable.\n\nExample - without payload decryption or decoding:\n\n```python\nfrom pyspartn import SPARTNReader\n\ntransport = b\"s\\x00\\x12\\xe2\\x00|\\x10[\\x12H\\xf5\\t\\xa0\\xb4+\\x99\\x02\\x15\\xe2\\x05\\x85\\xb7\\x83\\xc5\\xfd\\x0f\\xfe\\xdf\\x18\\xbe\\x7fv \\xc3`\\x82\\x98\\x10\\x07\\xdc\\xeb\\x82\\x7f\\xcf\\xf8\\x9e\\xa3ta\\xad\"\nmsg = SPARTNReader.parse(transport, decode=0)\nprint(msg)\n```\n```\n<SPARTN(SPARTN-1X-OCB-GPS, msgType=0, nData=37, eaf=1, crcType=2, frameCrc=2, msgSubtype=0, timeTagtype=0, gnssTimeTag=3970, solutionId=5, solutionProcId=11, encryptionId=1, encryptionSeq=9, authInd=1, embAuthLen=0, crc=7627181, )>\n```\n\nExample - with payload decryption and decoding (requires key and, for messages where `timeTagtype=0`, a nominal basedate):\n\n```python\nfrom datetime import datetime\nfrom pyspartn import SPARTNReader\n\ntransport = b\"\\x73\\x04\\x19\\x62\\x03\\xfa\\x20\\x5b\\x1f\\xc8\\x31\\x0b\\x03\\xd3\\xa4\\xb1\\xdb\\x79\\x21\\xcb\\x5c\\x27\\x12\\xa7\\xa8\\xc2\\x52\\xfd\\x4a\\xfb\\x1a\\x96\\x3b\\x64\\x2a\\x4e\\xcd\\x86\\xbb\\x31\\x7c\\x61\\xde\\xf5\\xdb\\x3d\\xa3\\x2c\\x65\\xd5\\x05\\x9f\\x1c\\xd9\\x96\\x47\\x3b\\xca\\x13\\x5e\\x5e\\x54\\x80\"\nmsg = SPARTNReader.parse(\n    transport,\n    decode=1,\n    key=\"6b30302427df05b4d98911ebff3a4d95\",\n    basedate=datetime(2023, 6, 27, 22, 3, 0),\n)\nprint(msg)\n```\n```\n<SPARTN(SPARTN-1X-GAD, msgType=2, nData=50, eaf=1, crcType=2, frameCrc=2, msgSubtype=0, timeTagtype=0, gnssTimeTag=32580, solutionId=5, solutionProcId=11, encryptionId=1, encryptionSeq=63, authInd=1, embAuthLen=0, crc=6182016, SF005=37, SF068=1, SF069=0, SF030=7, SF031_01=32, SF032_01=43.20000000000002, SF033_01=18.700000000000017, SF034_01=6, SF035_01=2, SF036_01=0.6, SF037_01=2.3000000000000003, SF031_02=33, SF032_02=43.20000000000002, SF033_02=23.30000000000001, SF034_02=6, SF035_02=3, SF036_02=0.6, SF037_02=1.7000000000000002, SF031_03=34, SF032_03=40.099999999999994, SF033_03=12.100000000000023, SF034_03=2, SF035_03=6, SF036_03=1.9000000000000001, SF037_03=1.1, SF031_04=35, SF032_04=39.70000000000002, SF033_04=18.700000000000017, SF034_04=3, SF035_04=3, SF036_04=1.3000000000000003, SF037_04=2.3000000000000003, SF031_05=36, SF032_05=54.80000000000001, SF033_05=-3.1999999999999886, SF034_05=6, SF035_05=2, SF036_05=0.6, SF037_05=3.1, SF031_06=37, SF032_06=49.099999999999994, SF033_06=-5.5, SF034_06=4, SF035_06=7, SF036_06=0.8, SF037_06=1.1, SF031_07=38, SF032_07=46.0, SF033_07=10.600000000000023, SF034_07=3, SF035_07=2, SF036_07=0.9, SF037_07=2.3000000000000003, SF031_08=39, SF032_08=46.0, SF033_08=1.8000000000000114, SF034_08=7, SF035_08=2, SF036_08=0.7000000000000001, SF037_08=2.3000000000000003)>\n```\n\nThe `SPARTNMessage` object exposes different public attributes depending on its message type or 'identity'. SPARTN data fields are denoted `SFnnn` - use the `datadesc()` helper method to obtain a more user-friendly text description of the data field.\n\n```python\nfrom datetime import datetime\nfrom pyspartn import SPARTNReader, datadesc\nmsg = SPARTNReader.parse(b\"s\\x02\\xf7\\xeb\\x08\\xd7!\\xef\\x80[\\x17\\x88\\xc2?\\x0f\\x ... \\xc4#fFy\\xb9\\xd5\", decode=True, key=\"930d847b779b126863c8b3b2766ae7cc\", basedate=datetime(2024, 4, 18, 20, 48, 29, 977255))\nprint(msg)\nprint(msg.identity)\nprint(msg.gnssTimeTag)\nprint(datadesc(\"SF005\"), msg.SF005)\nprint(datadesc(\"SF061a\"), msg.SF061a_10_05)\n```\n```\n<SPARTN(SPARTN-1X-HPAC-GPS, msgType=1, nData=495, eaf=1, crcType=2, frameCrc=11, msgSubtype=0, timeTagtype=1, gnssTimeTag=451165680, solutionId=5, solutionProcId=11, encryptionId=1, encryptionSeq=30, authInd=1, embAuthLen=0, crc=7977429, SF005=152, SF068=1, SF069=0, SF030=9, SF031_01=0, SF039_01=0, SF040T_01=1, SF040I_01=1, SF041_01=1, SF042_01=1, SF043_01=0.0, SF044_01=1, SF048_01=-0.21199999999999997, SF049a_01=0.0, SF049b_01=0.0010000000000000009, SF054_01=1, SatBitmaskLen_01=0, SF011_01=880836738, SF055_01_01=1, SF056_01_01=1, SF060_01_01=-11.120000000000005, ..., SF061a_10_05=-0.27200000000000557, SF061b_10_05=0.1839999999999975, SF055_10_06=2, SF056_10_06=1, SF060_10_06=7.640000000000043, SF061a_10_06=-1.3840000000000003, SF061b_10_06=-0.7920000000000016)>\n'SPARTN-1X-HPAC-GPS'\n451165680\n('Solution issue of update (SIOU)', 152)\n('Large ionosphere coefficient C01', -0.27200000000000557)\n```\n\nAttributes in nested repeating groups are suffixed with a 2-digit index for each nested level e.g. `SF032_06`, `SF061a_10_05`. To iterate through nested grouped attributes, you can use a construct similar to the following (_this example iterates through SF032 Area reference latitude values in a SPARTN-1X-GAD message_):\n\n```python\nvals = []\nfor i in range(parsed_data.SF030 + 1):  # attribute or formula representing group size\n    vals.append(getattr(parsed_data, f\"SF032_{i+1:02d}\"))\nprint(vals)\n```\n\nEnumerations for coded values can be found in [spartntables.py](https://github.com/semuconsulting/pyspartn/blob/main/src/pyspartn/spartntables.py).\n\nThe `payload` attribute always contains the raw payload as bytes.\n\n---\n## <a name=\"generating\">Generating</a>\n\n```\nclass pyspartn.spartnmessage.SPARTNMessage(**kwargs)\n```\n\nYou can create an `SPARTNMessage` object by calling the constructor with the following keyword arguments:\n1. transport as bytes\n\nExample:\n\n```python\nfrom pyspartn import SPARTNMessage\nmsg = SPARTNMessage(transport=b\"s\\x00\\x12\\xe2\\x00|\\x10[\\x12H\\xf5\\t\\xa0\\xb4+\\x99\\x02\\x15\\xe2\\x05\\x85\\xb7\\x83\\xc5\\xfd\\x0f\\xfe\\xdf\\x18\\xbe\\x7fv \\xc3`\\x82\\x98\\x10\\x07\\xdc\\xeb\\x82\\x7f\\xcf\\xf8\\x9e\\xa3ta\\xad\")\nprint(msg)\n```\n```\n<SPARTN(SPARTN-1X-OCB-GPS, msgType=0, nData=37, eaf=1, crcType=2, frameCrc=2, msgSubtype=0, timeTagtype=0, gnssTimeTag=3970, solutionId=5, solutionProcId=11, encryptionId=1, encryptionSeq=9, authInd=1, embAuthLen=0, crc=7627181, )>\n```\n\n---\n## <a name=\"serializing\">Serializing</a>\n\nThe `SPARTNMessage` class implements a `serialize()` method to convert a `SPARTNMMessage` object to a bytes array suitable for writing to an output stream.\n\ne.g. to create and send a SPARTN-1X-OCB-GPS message type:\n\n```python\nfrom serial import Serial\nserialOut = Serial('/dev/ttyACM1', 38400, timeout=5)\nfrom pyspartn import SPARTNMessage\nmsg = SPARTNMessage(transport=b\"s\\x00\\x12\\xe2\\x00|\\x10[\\x12H\\xf5\\t\\xa0\\xb4+\\x99\\x02\\x15\\xe2\\x05\\x85\\xb7\\x83\\xc5\\xfd\\x0f\\xfe\\xdf\\x18\\xbe\\x7fv \\xc3`\\x82\\x98\\x10\\x07\\xdc\\xeb\\x82\\x7f\\xcf\\xf8\\x9e\\xa3ta\\xad\")\nprint(msg)\noutput = msg.serialize()\nprint(output)\nserialOut.write(output)\n```\n```\n<SPARTN(SPARTN-1X-OCB-GPS, msgType=0, nData=37, eaf=1, crcType=2, frameCrc=2, msgSubtype=0, timeTagtype=0, gnssTimeTag=3970, solutionId=5, solutionProcId=11, encryptionId=1, encryptionSeq=9, authInd=1, embAuthLen=0, crc=7627181, )>\nb's\\x00\\x12\\xe2\\x00|\\x10[\\x12H\\xf5\\t\\xa0\\xb4+\\x99\\x02\\x15\\xe2\\x05\\x85\\xb7\\x83\\xc5\\xfd\\x0f\\xfe\\xdf\\x18\\xbe\\x7fv \\xc3`\\x82\\x98\\x10\\x07\\xdc\\xeb\\x82\\x7f\\xcf\\xf8\\x9e\\xa3ta\\xad'\n```\n\n---\n## <a name=\"examples\">Examples</a>\n\nThe following examples are available in the /examples folder:\n\n1. `spartn_mqtt_client.py` - implements a simple SPARTN MQTT client using the pygnssutils.GNSSMQTTClient class. **NB**: requires a valid ClientID for a\nSPARTN MQTT service e.g. u-blox Thingstream PointPerfect MQTT.\n1. `spartn_ntrip_client.py` - implements a simple SPARTN NTRIP client using the pygnssutils.GNSSNTRIPClient class. **NB**: requires a valid user and password for a\nSPARTN NTRIP service e.g. u-blox Thingstream PointPerfect NTRIP.\n1. `spartn_decrypt.py` - illustrates how to read, decrypt and decode a binary SPARTN log file (e.g. from the `spartn_mqtt_client.py` or `spartn_ntrip_client.py` examples above).\n1. `rxmpmp_extract_spartn.py` - ilustrates how to extract individual SPARTN messages from the accumulated UBX-RXM-PMP data output by an NEO-D9S L-band correction receiver.\n1. `spartnparser.py` - illustrates how to parse SPARTN transport layer data from the binary SPARTN messages output by the `rxmpmp_extract_spartn.py` above.\n1. `gad_plot.py` - illustrates how to extract geographic area definitions from a series of SPARTN-GAD-1X messages - the output file from the example above can be used as an input. This example also serves to illustrate how to decrypt SPARTN messages.\n\n---\n## <a name=\"troubleshooting\">Troubleshooting</a>\n\n1. `SPARTNTypeError` or `SPARTNParseError` when parsing encrypted messages with 16-bit gnssTimetags (`timeTagtype=0`), e.g. GAD or some OCB messages:\n\n   ```\n   pyspartn.exceptions.SPARTNTypeError: Error processing attribute 'group' in message type SPARTN-1X-GAD\n   ```\n\n   This is almost certainly due to an invalid decryption key and/or basedate. Remember that keys are only valid for a 4 week period, and basedates are valid for no more than half a day. Note also that different GNSS constellations use different UTC datums e.g. GLONASS timestamps are based on UTC+3. Check with your SPARTN service provider for the latest decryption key(s), and check the original creation date of your SPARTN datasource.\n\n1. `SSL: CERTIFICATE_VERIFY_FAILED` error when attempting to connect to SPARTN MQTT service using `gnssmqttclient` on MacOS:\n\n   ```\n   [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1000)\n   ```\n\n   This is because `gnssmqttclient` is unable to locate the RootCA certificate for the MQTT Broker. This can normally be resolved as follows:\n   - Install the latest version of certifi: ```python3 -m pip install --upgrade certifi```\n   - Run the following command from the terminal (_substituting your Python path and version as required_): ```/Applications/Python\\ 3.12/Install\\ Certificates.command```\n\n1. Unable to install `crytography` library required by `pyspartn` on 32-bit Linux platforms:\n\n   ```\n   Building wheel for cryptography (PEP 517): started\n   Building wheel for cryptography (PEP 517): finished with status 'error'\n   ```\n\n   Refer to [cryptography installation README.md](https://github.com/semuconsulting/pyspartn/blob/main/cryptography_installation/README.md).\n\n1. Checking for successful decryption. `SPARTNMessage` objects implement a protected attribute `_padding`, which represents the number of redundant bits added to the payload content in order to byte-align the payload with the number of bytes specified in the transport layer payload length attribute `nData`. If the payload has been successfully decrypted and decoded, the value of `_padding` should always be between 0 and 8. Checking `0 <= msg._padding <= 8` provides an informal (_but not necessarily definitive_) check of successful decryption and decoding (see, for example, [spartn_decrypt.py](https://github.com/semuconsulting/pyspartn/blob/main/examples/spartn_decrypt.py)).\n\n---\n## <a name=\"gui\">Graphical Client</a>\n\nA python/tkinter graphical GPS client which supports NMEA, UBX, RTCM3 and SPARTN protocols is available at: \n\n[https://github.com/semuconsulting/PyGPSClient](https://github.com/semuconsulting/PyGPSClient)\n\n---\n## <a name=\"author\">Author & License Information</a>\n\nsemuadmin@semuconsulting.com\n\n![License](https://img.shields.io/github/license/semuconsulting/pyspartn.svg)\n\n`pyspartn` is maintained entirely by unpaid volunteers. It receives no funding from advertising or corporate sponsorship. If you find the library useful, a small donation would be greatly appreciated!\n\n[![Donations](https://www.paypalobjects.com/en_GB/i/btn/btn_donate_LG.gif)](https://www.paypal.com/donate/?business=UL24WUA4XHNRY&no_recurring=0&item_name=The+SEMU+GNSS+Python+libraries+are+maintained+entirely+by+unpaid+volunteers.+All+donations+are+greatly+appreciated.&currency_code=GBP)\n",
    "bugtrack_url": null,
    "license": "BSD 3-Clause License (\"BSD License 2.0\", \"Revised BSD License\", \"New BSD License\", or \"Modified BSD License\")  Copyright (c) 2023, SEMU Consulting All rights reserved.  Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the <organization> nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ",
    "summary": "SPARTN protocol parser",
    "version": "0.4.0",
    "project_urls": {
        "changelog": "https://github.com/semuconsulting/pyspartn/blob/master/RELEASE_NOTES.md",
        "documentation": "https://www.semuconsulting.com/pyspartn/",
        "homepage": "https://github.com/semuconsulting/pyspartn",
        "repository": "https://github.com/semuconsulting/pyspartn"
    },
    "split_keywords": [],
    "urls": [
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "979a81ec29df3086b900b097cceb5ccdf7e5e89556dc9a505a5a3529908a910a",
                "md5": "765e8b86779c7a1d5e63de7d72118857",
                "sha256": "b2d2342e921d75243c18f357c56f482b1e2b170b668c12f9e052f9ae1eaa9503"
            },
            "downloads": -1,
            "filename": "pyspartn-0.4.0-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "765e8b86779c7a1d5e63de7d72118857",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": ">=3.8",
            "size": 31710,
            "upload_time": "2024-05-01T09:04:18",
            "upload_time_iso_8601": "2024-05-01T09:04:18.924717Z",
            "url": "https://files.pythonhosted.org/packages/97/9a/81ec29df3086b900b097cceb5ccdf7e5e89556dc9a505a5a3529908a910a/pyspartn-0.4.0-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "727b8b3c801ca8c00942be15f0bfc01481e21844ade47409f79ea5177ca3e56b",
                "md5": "4cf912086b112810a2bf60240e574699",
                "sha256": "62a89261daa106fc614f5de39c429915815b74e2c0ee1f1af421e35573dec4b0"
            },
            "downloads": -1,
            "filename": "pyspartn-0.4.0.tar.gz",
            "has_sig": false,
            "md5_digest": "4cf912086b112810a2bf60240e574699",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": ">=3.8",
            "size": 71111,
            "upload_time": "2024-05-01T09:04:20",
            "upload_time_iso_8601": "2024-05-01T09:04:20.672466Z",
            "url": "https://files.pythonhosted.org/packages/72/7b/8b3c801ca8c00942be15f0bfc01481e21844ade47409f79ea5177ca3e56b/pyspartn-0.4.0.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2024-05-01 09:04:20",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "semuconsulting",
    "github_project": "pyspartn",
    "travis_ci": false,
    "coveralls": false,
    "github_actions": true,
    "lcname": "pyspartn"
}
        
Elapsed time: 0.29088s