diff --git a/.vscode/settings.json b/.vscode/settings.json index 158dbcf..134caed 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,5 +4,5 @@ "editor.formatOnSave": true, "modulename": "${workspaceFolderBasename}", "distname": "${workspaceFolderBasename}", - "moduleversion": "1.0.40", + "moduleversion": "1.0.41", } \ No newline at end of file diff --git a/README.md b/README.md index d606bc4..d5f88b3 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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) +``` +``` + +``` + --- ## Generating @@ -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: diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 2c128f2..efad57f 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,8 +1,42 @@ # pynmeagps Release Notes +### RELEASE 1.0.41 + +ENHANCEMENTS: + +1. Enhance NMEAMessage to parse unrecognised* NMEA sentence types to a nominal `` 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) + ``` + ``` + + ``` + + 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. diff --git a/pyproject.toml b/pyproject.toml index f87f106..1241c4c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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" diff --git a/src/pynmeagps/_version.py b/src/pynmeagps/_version.py index 5b76a94..bcae195 100644 --- a/src/pynmeagps/_version.py +++ b/src/pynmeagps/_version.py @@ -8,4 +8,4 @@ :license: BSD 3-Clause """ -__version__ = "1.0.40" +__version__ = "1.0.41" diff --git a/src/pynmeagps/nmeamessage.py b/src/pynmeagps/nmeamessage.py index e82b0b1..f4eaf3f 100644 --- a/src/pynmeagps/nmeamessage.py +++ b/src/pynmeagps/nmeamessage.py @@ -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 @@ -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. @@ -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( @@ -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 @@ -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 @@ -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. @@ -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 @@ -300,6 +328,8 @@ def __str__(self) -> str: stg = f"