Skip to content

Commit

Permalink
Merge pull request #65 from semuconsulting/RC-1.0.41
Browse files Browse the repository at this point in the history
parse unknown message IDs
  • Loading branch information
semuadmin authored Sep 15, 2024
2 parents 667dc92 + 4a09ba2 commit 3814785
Show file tree
Hide file tree
Showing 11 changed files with 262 additions and 61 deletions.
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 = "semuadmin@semuconsulting.com" }]
maintainers = [{ name = "semuadmin", email = "semuadmin@semuconsulting.com" }]
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

0 comments on commit 3814785

Please sign in to comment.