Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

parse unknown message IDs #65

Merged
merged 3 commits into from
Sep 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@
"editor.formatOnSave": true,
"modulename": "${workspaceFolderBasename}",
"distname": "${workspaceFolderBasename}",
"moduleversion": "1.0.40",
"moduleversion": "1.0.41",
}
20 changes: 16 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ with open('nmeadata.log', 'rb') as stream:
print(parsed_data)
```

Example - Socket input (using iterator):
* Socket input (using iterator):

```python
import socket
Expand Down Expand Up @@ -186,6 +186,18 @@ print(latlon2dmm((msg.lat, msg.lon)))
('52°37.2378′N', '2°9.6072′W')
```

If the NMEA sentence type is unrecognised or not yet implemented (*e.g. due to definition not yet being in the public domain*) and the `VALMSGID` validation flag is *NOT* set,
`NMEAMessage` will parse the message to a NOMINAL structure e.g.:

```python
from pynmeagps import NMEAReader, VALCKSUM
msg = NMEAReader.parse('$GNACN,103607.00,ECN,E,A,W,A,test,C*67\r\n', validate=VALCKSUM)
print(msg)
```
```
<NMEA(GNACN, NOMINAL, field_01=103607.00, field_02=ECN, field_03=E, field_04=A, field_05=W, field_06=A, field_07=test, field_08=C)>
```

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

Expand All @@ -195,9 +207,9 @@ class pynmeagps.nmeamessage.NMEAMessage(talker: str, msgID: str, msgmode: int, *

You can create an `NMEAMessage` object by calling the constructor with the following parameters:
1. talker (must be a valid talker from `pynmeagps.NMEA_TALKERS`)
1. message id (must be a valid id from `pynmeagps.NMEA_MSGIDS` or `pynmeagps.NMEA_MSGIDS_PROP`)
2. msgmode (0=GET, 1=SET, 2=POLL)
3. (optional) a series of keyword parameters representing the message payload
2. message id (must be a valid id from `pynmeagps.NMEA_MSGIDS` or `pynmeagps.NMEA_MSGIDS_PROP`)
3. msgmode (0=GET, 1=SET, 2=POLL)
4. (optional) a series of keyword parameters representing the message payload

The 'msgmode' parameter signifies whether the message payload refers to a:

Expand Down
36 changes: 35 additions & 1 deletion RELEASE_NOTES.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,42 @@
# pynmeagps Release Notes

### RELEASE 1.0.41

ENHANCEMENTS:

1. Enhance NMEAMessage to parse unrecognised* NMEA sentence types to a nominal `<NMEA(TTXXX, NOMINAL, field_01=x...)>` message structure if `VALMSGID` validation flag is *not* set, rather than raise a `NMEAParseMessage` error e.g.:

A. with the `VALMSGID` flag *not* set (*the new default behaviour*):

```shell
from pynmeagps import NMEAReader
msg = NMEAReader.parse("$GNACN,103607.00,ECN,E,A,W,A,test,C*67\r\n")
print(msg)
```
```
<NMEA(GNACN, NOMINAL, field_01=103607.00, field_02=ECN, field_03=E, field_04=A, field_05=W, field_06=A, field_07=test, field_08=C)>
```

B. with the `VALMSGID flag` set:

```shell
from pynmeagps import NMEAReader, VALMSGID
msg = NMEAReader.parse("$GNACN,103607.00,ECN,E,A,W,A,test,C*67\r\n", validate=VALMSGID)
print(msg)
```
```
pynmeagps.exceptions.NMEAParseError: Unknown msgID GNACN, msgmode GET.
```

\* unrecognised message types include those with unknown or invalid NMEA msgIDs (*but valid payloads and checksums*), or valid NMEA sentences whose payload definitions are not yet in the public domain (e.g. those currently commented-out in [`NMEA_MSGIDS`](https://github.com/semuconsulting/pynmeagps/blob/master/src/pynmeagps/nmeatypes_core.py#L207)).

1. Add NMEA ALF sentence definition.
1. Add `validate` argument to `NMEAMessage` and carry forward from `NMEAReader`
1. Add logger to `NMEAMessage`.

### RELEASE 1.0.40

CHANGES:
ENHANCEMENTS:

1. Add area() helper method to calculate spherical area of bounding box.
1. Sphinx documentation and docstrings enhanced to include global constants and decodes.
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ name = "pynmeagps"
authors = [{ name = "semuadmin", email = "[email protected]" }]
maintainers = [{ name = "semuadmin", email = "[email protected]" }]
description = "NMEA protocol parser and generator"
version = "1.0.40"
version = "1.0.41"
license = { file = "LICENSE" }
readme = "README.md"
requires-python = ">=3.8"
Expand Down
2 changes: 1 addition & 1 deletion src/pynmeagps/_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@
:license: BSD 3-Clause
"""

__version__ = "1.0.40"
__version__ = "1.0.41"
44 changes: 37 additions & 7 deletions src/pynmeagps/nmeamessage.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

import struct
from datetime import datetime, timezone
from logging import getLogger

import pynmeagps.exceptions as nme
import pynmeagps.nmeatypes_core as nmt
Expand All @@ -33,7 +34,13 @@ class NMEAMessage:
"""NMEA GNSS/GPS Message Class."""

def __init__(
self, talker: str, msgID: str, msgmode: int, hpnmeamode: bool = False, **kwargs
self,
talker: str,
msgID: str,
msgmode: int,
hpnmeamode: bool = False,
validate: int = nmt.VALCKSUM,
**kwargs,
):
"""Constructor.

Expand All @@ -47,12 +54,16 @@ def __init__(
:param str msgID: message ID e.g. "GGA"
:param int msgmode: mode (0=GET, 1=SET, 2=POLL)
:param bool hpnmeamode: high precision lat/lon mode (7dp rather than 5dp) (False)
:param int validate: validation flags - VALNONE (0), VALCKSUM (1), VALMSGID (2) (1)
:param kwargs: keyword arg(s) representing all or some payload attributes
:raises: NMEAMessageError
"""

# object is mutable during initialisation only
super().__setattr__("_immutable", False)
self._logger = getLogger(__name__)
self._validate = validate
self._nominal = False # flag for unrecognised NMEA sentence types

if msgmode not in (0, 1, 2):
raise nme.NMEAMessageError(
Expand All @@ -65,9 +76,10 @@ def __init__(
and msgID not in (nmt.NMEA_MSGIDS_PROP)
and msgID not in (nmt.PROP_MSGIDS)
):
raise nme.NMEAMessageError(
f"Unknown msgID {talker}{msgID}, msgmode {('GET','SET','POLL')[msgmode]}."
)
if self._validate & nmt.VALMSGID:
raise nme.NMEAMessageError(
f"Unknown msgID {talker}{msgID}, msgmode {('GET','SET','POLL')[msgmode]}."
)

self._mode = msgmode
# high precision NMEA mode returns NMEA lat/lon to 7dp rather than 5dp
Expand All @@ -93,6 +105,10 @@ def _do_attributes(self, **kwargs):
self._payload = kwargs.get("payload", [])
self._checksum = kwargs.get("checksum", None)
pdict = self._get_dict(**kwargs) # get payload definition dict
if pdict is None: # definition not yet implemented
if "payload" in kwargs:
self._set_attribute_nominal(kwargs["payload"])
return
for key in pdict.keys(): # process each attribute in dict
(pindex, gindex) = self._set_attribute(
pindex, pdict, key, gindex, **kwargs
Expand Down Expand Up @@ -238,6 +254,17 @@ def _set_attribute_single(

return pindex

def _set_attribute_nominal(self, payload: list):
"""
Set nominal attributes for unrecognised NMEA sentence types.

:param list payload: payload as list
"""

self._nominal = True
for i, fld in enumerate(payload):
setattr(self, f"field_{i+1:02d}", fld)

def _get_dict(self, **kwargs) -> dict:
"""
Get payload dictionary.
Expand Down Expand Up @@ -268,9 +295,10 @@ def _get_dict(self, **kwargs) -> dict:
return nms.NMEA_PAYLOADS_SET[key]
return nmg.NMEA_PAYLOADS_GET[key]
except KeyError as err:
raise nme.NMEAMessageError(
f"Unknown msgID {key} msgmode {('GET', 'SET', 'POLL')[self._mode]}."
) from err
erm = f"Unknown msgID {key} msgmode {('GET', 'SET', 'POLL')[self._mode]}."
if self._validate & nmt.VALMSGID:
raise nme.NMEAMessageError(erm) from err
return None # message not yet implemented

def _calc_num_repeats(
self, attd: dict, payload: list, pindex: int, pindexend: int = 0
Expand Down Expand Up @@ -300,6 +328,8 @@ def __str__(self) -> str:

stg = f"<NMEA({self.identity}"
stg += ", "
if self._nominal:
stg += "NOMINAL, "
for i, att in enumerate(self.__dict__):
if att[0] != "_": # only show public attributes
val = self.__dict__[att]
Expand Down
13 changes: 9 additions & 4 deletions src/pynmeagps/nmeareader.py
Original file line number Diff line number Diff line change
Expand Up @@ -250,10 +250,15 @@ def parse(
f" - should be {ccksum}."
)
return NMEAMessage(
talker, msgid, msgmode, payload=payload, checksum=checksum
talker,
msgid,
msgmode,
payload=payload,
checksum=checksum,
validate=validate,
)

except nme.NMEAMessageError as err:
if not validate & VALMSGID:
return None
raise nme.NMEAParseError(err)
if validate & VALMSGID:
raise nme.NMEAParseError(err)
return None
Loading