From bc2cf0e8e4bdd62b1ffead0bd8dee4b236be49f5 Mon Sep 17 00:00:00 2001
From: pelter <35609229+pelter@users.noreply.github.com>
Date: Sat, 2 Feb 2019 22:15:42 +0000
Subject: [PATCH 01/35] Minor typographical updates in comments (#73)

---
 pynmea2/nmea_utils.py | 14 +++++++-------
 1 file changed, 7 insertions(+), 7 deletions(-)

diff --git a/pynmea2/nmea_utils.py b/pynmea2/nmea_utils.py
index 02d3589..fb2b060 100644
--- a/pynmea2/nmea_utils.py
+++ b/pynmea2/nmea_utils.py
@@ -3,7 +3,7 @@
 
 def timestamp(s):
     '''
-    Converts a timestamp given in "hhmmss[.ss]" ASCII format to a
+    Converts a timestamp given in "hhmmss[.ss]" ASCII text format to a
     datetime.time object
     '''
     ms_s = s[6:]
@@ -19,7 +19,7 @@ def timestamp(s):
 
 def datestamp(s):
     '''
-    Converts a datestamp given in "DDMMYY" ASCII format to a
+    Converts a datestamp given in "DDMMYY" ASCII text format to a
     datetime.datetime object
     '''
     return datetime.datetime.strptime(s, '%d%m%y').date()
@@ -28,8 +28,8 @@ def datestamp(s):
 import re
 def dm_to_sd(dm):
     '''
-    Converts a geographic coordiante given in "degres/minutes" dddmm.mmmm
-    format (ie, "12319.943281" = 123 degrees, 19.953281 minutes) to a signed
+    Converts a geographic co-ordinate given in "degrees/minutes" dddmm.mmmm
+    format (eg, "12319.943281" = 123 degrees, 19.943281 minutes) to a signed
     decimal (python float) format
     '''
     # '12319.943281'
@@ -40,13 +40,13 @@ def dm_to_sd(dm):
 
 
 class LatLonFix(object):
-    '''Mixin to add `lattitude` and `longitude` properties as signed decimals
-    to NMEA sentences which have coordiantes given as degrees/minutes (lat, lon)
+    '''Mixin to add `latitude` and `longitude` properties as signed decimals
+    to NMEA sentences which have co-ordinates given as degrees/minutes (lat, lon)
     and cardinal directions (lat_dir, lon_dir)'''
     #pylint: disable=no-member
     @property
     def latitude(self):
-        '''Lattitude in signed degrees (python float)'''
+        '''Latitude in signed degrees (python float)'''
         sd = dm_to_sd(self.lat)
         if self.lat_dir == 'N':
             return +sd

From 2b80648bcb69b4db88153aafa9633318062cb9cb Mon Sep 17 00:00:00 2001
From: hetlelid <spam@hetlelid.no>
Date: Sat, 2 Feb 2019 23:17:57 +0100
Subject: [PATCH 02/35] Update README.md (#76)

---
 README.md | 1 +
 1 file changed, 1 insertion(+)

diff --git a/README.md b/README.md
index ba91344..0bf966d 100644
--- a/README.md
+++ b/README.md
@@ -97,6 +97,7 @@ Generating
 You can create a `NMEASentence` object by calling the constructor with talker, message type, and data fields:
 
 ```python
+>>> import pynmea2
 >>> msg = pynmea2.GGA('GP', 'GGA', ('184353.07', '1929.045', 'S', '02410.506', 'E', '1', '04', '2.6', '100.00', 'M', '-33.9', 'M', '', '0000'))
 ```
 

From 0c59280d5e5c86e9af8e19d478fc787f55530ef9 Mon Sep 17 00:00:00 2001
From: rdoumenc <remi.doumenc@gmail.com>
Date: Sat, 2 Feb 2019 23:20:25 +0100
Subject: [PATCH 03/35] MANIFEST.in: fix license file name (#80)

---
 MANIFEST.in | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/MANIFEST.in b/MANIFEST.in
index 4cb8ee8..fc00aff 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1,5 +1,5 @@
 include README.md
-include LICENSE.txt
+include LICENSE
 recursive-include test *
 recursive-include examples *
 global-exclude __pycache__

From a5039e623a553224b53c8024a1d1ccbb696002e3 Mon Sep 17 00:00:00 2001
From: Tom Flanagan <tom@zkpq.ca>
Date: Sat, 2 Feb 2019 14:30:14 -0800
Subject: [PATCH 04/35] Upgrade travis

---
 .travis.yml       | 8 +++-----
 test/test_file.py | 1 +
 2 files changed, 4 insertions(+), 5 deletions(-)

diff --git a/.travis.yml b/.travis.yml
index 3f278c6..be73367 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -2,7 +2,6 @@ language: python
 
 python:
   - "2.7"
-  - "3.3"
   - "3.4"
   - "3.5"
   - "3.6"
@@ -11,8 +10,9 @@ python:
 
 # command to install dependencies, e.g. pip install -r requirements.txt --use-mirrors
 install:
-    - pip install pytest>=2.7.3 --upgrade
-    - pip install pylint
+    - pip install pip --upgrade
+    - pip install pytest --upgrade
+    - pip install pylint --upgrade
 
 # command to run tests, e.g. python setup.py test
 script:
@@ -26,5 +26,3 @@ after_success:
     - PYTHONPATH=. coverage run --source=pynmea2 -m pytest
     - coverage report
     - coveralls
-
-sudo: false
diff --git a/test/test_file.py b/test/test_file.py
index 003de25..0858e38 100644
--- a/test/test_file.py
+++ b/test/test_file.py
@@ -16,6 +16,7 @@
 $GPGLL,4040.018,N,07808.022,W,181039.576,V*39
 $GPRMC,181040.576,V,4133.618,N,07725.034,W,96.8,44.47,250915,,E*7F"""
 
+
 def test_file():
     nmeafile = pynmea2.NMEAFile(StringIO(TEST_DATA))
 

From dddf8bed22b111cc8d78715b3bdd9996c4018120 Mon Sep 17 00:00:00 2001
From: Tom Flanagan <tom@zkpq.ca>
Date: Sat, 2 Feb 2019 15:00:58 -0800
Subject: [PATCH 05/35] Bump version

---
 Makefile            | 17 +++++++++++------
 pynmea2/_version.py |  2 +-
 test/test_pynmea.py |  2 +-
 3 files changed, 13 insertions(+), 8 deletions(-)

diff --git a/Makefile b/Makefile
index 75bfc4e..7c66da9 100644
--- a/Makefile
+++ b/Makefile
@@ -1,6 +1,11 @@
-
-test:
-	py.test
-
-publish:
-	python setup.py sdist upload
+test:
+	python2 -m pytest .
+	python3 -m pytest .
+
+publish: test
+	rm dist/ -r
+	python3 setup.py sdist
+	python3 setup.py bdist_wheel
+	python3 -m twine upload dist/*
+
+.PHONY: test publish
diff --git a/pynmea2/_version.py b/pynmea2/_version.py
index 5ca0f70..2699116 100644
--- a/pynmea2/_version.py
+++ b/pynmea2/_version.py
@@ -1 +1 @@
-__version__ = '1.12.0'
+__version__ = '1.14.0'
diff --git a/test/test_pynmea.py b/test/test_pynmea.py
index 941aa7b..1df6217 100644
--- a/test/test_pynmea.py
+++ b/test/test_pynmea.py
@@ -5,7 +5,7 @@
 
 
 def test_version():
-    version = '1.12.0'
+    version = '1.14.0'
     assert pynmea2.version == version
     assert pynmea2.__version__ == version
 

From 52d217f9085ce1a570243e27d497fe8296d9299a Mon Sep 17 00:00:00 2001
From: xOneca <xoneca@gmail.com>
Date: Sun, 3 Feb 2019 21:30:43 +0100
Subject: [PATCH 06/35] NMEAStreamReader: support iterator protocol (#82)

* NMEAStreamReader: support iterator protocol.

This allows NMEAStreamReader object to be used in a for loop:

for batch in NMEAStreamReader(stream):
    for msg in batch:
        print msg

* Python 3 iterator compatibility

Python 3 uses __next__ method.

* Add test_iter

Add a test for the new iterator interface.

* Respect test separation

No code changes.
---
 pynmea2/stream.py   | 14 ++++++++++++++
 test/test_stream.py |  9 +++++++++
 2 files changed, 23 insertions(+)

diff --git a/pynmea2/stream.py b/pynmea2/stream.py
index e56814d..31970fb 100644
--- a/pynmea2/stream.py
+++ b/pynmea2/stream.py
@@ -59,3 +59,17 @@ def next(self, data=None):
                     yield e
                 if self.errors == 'ignore':
                     pass
+
+    __next__ = next
+
+    def __iter__(self):
+        '''
+        Support the iterator protocol.
+
+        This allows NMEAStreamReader object to be used in a for loop.
+
+          for batch in NMEAStreamReader(stream):
+              for msg in batch:
+                  print msg
+        '''
+        return self
diff --git a/test/test_stream.py b/test/test_stream.py
index 40eac47..24f4f07 100644
--- a/test/test_stream.py
+++ b/test/test_stream.py
@@ -31,6 +31,15 @@ def test_stream():
     assert len(list(sr.next())) == 0
 
 
+def test_iter():
+    sr = pynmea2.NMEAStreamReader(StringIO(DATA))
+    for batch in sr:
+        for msg in batch:
+            assert isinstance(msg, pynmea2.GGA)
+            break
+        break
+
+
 def test_raise_errors():
     sr = pynmea2.NMEAStreamReader(errors='raise')
     assert list(sr.next('foobar')) == []

From c56706b450256fa5a49c66fb4bb461025b974944 Mon Sep 17 00:00:00 2001
From: Tom Flanagan <tom@zkpq.ca>
Date: Sun, 3 Feb 2019 12:31:51 -0800
Subject: [PATCH 07/35] Bump version to 1.15.0

---
 pynmea2/_version.py | 2 +-
 test/test_pynmea.py | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/pynmea2/_version.py b/pynmea2/_version.py
index 2699116..6819333 100644
--- a/pynmea2/_version.py
+++ b/pynmea2/_version.py
@@ -1 +1 @@
-__version__ = '1.14.0'
+__version__ = '1.15.0'
diff --git a/test/test_pynmea.py b/test/test_pynmea.py
index 1df6217..cdcd4cf 100644
--- a/test/test_pynmea.py
+++ b/test/test_pynmea.py
@@ -5,7 +5,7 @@
 
 
 def test_version():
-    version = '1.14.0'
+    version = '1.15.0'
     assert pynmea2.version == version
     assert pynmea2.__version__ == version
 

From 2f634c261416693bad6eeeeae4ccb8f9d9eb2c6d Mon Sep 17 00:00:00 2001
From: Tom Flanagan <tom@zkpq.ca>
Date: Tue, 5 Feb 2019 15:05:09 -0800
Subject: [PATCH 08/35] Update README.md

---
 README.md | 13 +++++--------
 1 file changed, 5 insertions(+), 8 deletions(-)

diff --git a/README.md b/README.md
index 0bf966d..83048ed 100644
--- a/README.md
+++ b/README.md
@@ -7,13 +7,9 @@ pynmea2
 
 The `pynmea2` homepage is located at http://github.com/Knio/pynmea2
 
-
-### Compatibility
-
-`pynmea2` is compatable with Python 2.7 and Python 3.3
-
-[![Build Status](https://travis-ci.org/Knio/pynmea2.png?branch=master)](https://travis-ci.org/Knio/pynmea2)
-[![Coverage Status](https://coveralls.io/repos/Knio/pynmea2/badge.png?branch=master)](https://coveralls.io/r/Knio/pynmea2?branch=master)
+![Python version](https://img.shields.io/pypi/pyversions/pynmea2.svg?style=flat)
+[![Build status](https://img.shields.io/travis/Knio/pynmea2/master.svg?style=flat)](https://travis-ci.org/Knio/pynmea2)
+[![Coverage status](https://img.shields.io/coveralls/github/Knio/pynmea2/master.svg?style=flat)](https://coveralls.io/r/Knio/pynmea2?branch=master)
 [![Code Health](https://landscape.io/github/Knio/pynmea2/master/landscape.svg?style=flat)](https://landscape.io/github/Knio/pynmea2/master)
 
 ### Installation
@@ -23,7 +19,8 @@ The recommended way to install `pynmea2` is with
 
     pip install pynmea2
 
-[![PyPI version](https://badge.fury.io/py/pynmea2.png)](http://badge.fury.io/py/pynmea2)
+[![PyPI version](https://img.shields.io/pypi/v/pynmea2.svg?style=flat)](https://pypi.org/project/pynmea2/)
+[![PyPI downloads](https://img.shields.io/pypi/dm/pynmea2.svg?style=flat)](https://pypi.org/project/pynmea2/)
 
 Parsing
 -------

From c4fc66c6a13dd85ad862b15c516245af6e571456 Mon Sep 17 00:00:00 2001
From: Tom Flanagan <tom@zkpq.ca>
Date: Tue, 5 Feb 2019 15:06:06 -0800
Subject: [PATCH 09/35] Update setup.py

---
 setup.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/setup.py b/setup.py
index ade345a..dc6b841 100644
--- a/setup.py
+++ b/setup.py
@@ -23,9 +23,9 @@
         'Programming Language :: Python :: 2',
         'Programming Language :: Python :: 2.7',
         'Programming Language :: Python :: 3',
-        'Programming Language :: Python :: 3.3',
         'Programming Language :: Python :: 3.4',
         'Programming Language :: Python :: 3.5',
+        'Programming Language :: Python :: 3.6',
         'Programming Language :: Python :: Implementation :: PyPy',
         'Topic :: Scientific/Engineering :: GIS',
         'Topic :: Software Development :: Libraries :: Python Modules',

From 5fd7e1c91a0f0b9e537a4d7347ce5c7895ac8a3f Mon Sep 17 00:00:00 2001
From: znoop333 <znoop333@hotmail.com>
Date: Wed, 26 Feb 2020 23:31:49 -0500
Subject: [PATCH 10/35] Proprietary extensions for Vectronix Moskito TI laser
 measurement device (#99)

Proprietary extensions for Vectronix Moskito TI laser measurement device
---
 pynmea2/types/proprietary/__init__.py |  1 +
 pynmea2/types/proprietary/vtx.py      | 84 +++++++++++++++++++++++++++
 test/test_proprietary.py              | 24 ++++++++
 3 files changed, 109 insertions(+)
 create mode 100644 pynmea2/types/proprietary/vtx.py

diff --git a/pynmea2/types/proprietary/__init__.py b/pynmea2/types/proprietary/__init__.py
index b5db7c7..540ab50 100644
--- a/pynmea2/types/proprietary/__init__.py
+++ b/pynmea2/types/proprietary/__init__.py
@@ -5,4 +5,5 @@
 from . import sxn
 from . import tnl
 from . import ubx
+from . import vtx
 
diff --git a/pynmea2/types/proprietary/vtx.py b/pynmea2/types/proprietary/vtx.py
new file mode 100644
index 0000000..0e6c3c2
--- /dev/null
+++ b/pynmea2/types/proprietary/vtx.py
@@ -0,0 +1,84 @@
+# Vectronix Moskito TI (LRF)
+
+from decimal import Decimal
+
+from ... import nmea
+from ...nmea_utils import *
+
+class VTX(nmea.ProprietarySentence):
+    sentence_types = {}
+
+    def __new__(_cls, manufacturer, data):
+        name = manufacturer + data[1]
+        cls = _cls.sentence_types.get(name, _cls)
+        return super(VTX, cls).__new__(cls)
+
+    def __init__(self, manufacturer, data):
+        self.sentence_type = manufacturer + data[0]
+        super(VTX, self).__init__(manufacturer, data)
+
+
+class VTX0002(VTX):
+    """ Vectronix measurement: laser distance and angles (degrees) with declination
+    """
+    fields = (
+        ("Message Placeholder", "mplaceholder"),
+        ("Subtype", "subtype"),
+        ("Measurement ID", "measurement_id", int),
+        ("Distance (meters)", "dist", float),
+        ("Distance unit", "dist_unit"),
+        ("Direction (degrees)", "direction", float),
+        ("Direction unit", "direction_unit"),
+        ("Vertical angle (degrees)", "va", float),
+        ("Magnetic declination (degrees)", "decl", float),
+        ("Magnetic declination ref (E/W)", "decl_ref")
+    )
+
+
+class VTX0000(VTX):
+    """ Vectronix raw measurement: laser distance and angles (radians) without declination
+    """
+    fields = (
+        ("Message Placeholder", "mplaceholder"),
+        ("Subtype", "subtype"),
+        ("Distance (meters)", "dist", float),
+        ("Distance unit", "dist_unit"),
+        ("Direction (radians)", "direction", float),
+        ("Roll angle (radians)", "roll", float),
+        ("Vertical angle (radians)", "va", float),
+        ("Angular units type", "angle_units")
+    )
+
+
+class VTX0020(VTX, LatLonFix):
+    """ Vectronix self location: lat, long, altitude
+    """
+    fields = (
+        ("Message Placeholder", "mplaceholder"),
+        ("Subtype", "subtype"),
+        ("Measurement ID", "measurement_id", int),
+        ('Latitude', 'lat'),
+        ('Latitude Direction', 'lat_dir'),
+        ('Longitude', 'lon'),
+        ('Longitude Direction', 'lon_dir'),
+        ('Altitude above WGS84 ellipsoid, meters', 'altitude', float),
+        ('Altitude units', 'altitude_units')
+    )
+
+
+class VTX0012(VTX, LatLonFix):
+    """ Vectronix target location: lat, long, altitude, gain
+    """
+    fields = (
+        ("Message Placeholder", "mplaceholder"),
+        ("Subtype", "subtype"),
+        ("Measurement ID", "measurement_id", int),
+        ('Latitude', 'lat'),
+        ('Latitude Direction', 'lat_dir'),
+        ('Longitude', 'lon'),
+        ('Longitude Direction', 'lon_dir'),
+        ('Altitude above WGS84 ellipsoid, meters', 'altitude', float),
+        ('Altitude units', 'altitude_units'),
+        ('Gain (meters)', 'gain', float),
+        ('Gain units', 'gain_units')
+    )
diff --git a/test/test_proprietary.py b/test/test_proprietary.py
index 15f7c11..5522db9 100644
--- a/test/test_proprietary.py
+++ b/test/test_proprietary.py
@@ -171,3 +171,27 @@ def test_unknown_sentence():
     assert msg.manufacturer == 'ZZZ'
     assert msg.data == ['ABC', '1', '2', '3']
     assert msg.render(checksum=False, dollar=False) == data
+
+
+def test_proprietary_VTX_0002():
+    # A sample proprietary sentence from a Vectronix device (laser distance)
+    data = "$PVTX,0002,181330,00005.22,M,262.518,T,-01.967,09.358,W*7E"
+    msg = pynmea2.parse(data)
+    assert msg.manufacturer == 'VTX'
+    assert msg.dist == 5.22
+    assert msg.direction == 262.518
+    assert msg.va == -1.967
+    assert msg.render() == data
+
+
+def test_proprietary_VTX_0012():
+    # A sample proprietary sentence from a Vectronix device (target position)
+    data = "$PVTX,0012,177750,3348.5861,N,10048.5861,W,00045.2,M,038.8,M*22"
+    msg = pynmea2.parse(data)
+    assert msg.manufacturer == 'VTX'
+    assert msg.latitude == 33.80976833333333
+    assert msg.longitude == -100.80976833333334
+    assert msg.altitude == 45.2
+    assert msg.gain == 38.8
+    assert msg.render() == data
+

From c99dc295e23cd80d93e1bb02753ad2221b89fbd6 Mon Sep 17 00:00:00 2001
From: Col Ford <colin.ford@spirent.com>
Date: Sat, 21 Mar 2020 17:11:29 +0000
Subject: [PATCH 11/35] Added support for the GRS Talker

---
 pynmea2/types/talker.py | 20 ++++++++++++++++++++
 test/test_types.py      | 14 ++++++++++++++
 2 files changed, 34 insertions(+)

diff --git a/pynmea2/types/talker.py b/pynmea2/types/talker.py
index 32fe727..74b1d34 100644
--- a/pynmea2/types/talker.py
+++ b/pynmea2/types/talker.py
@@ -181,6 +181,26 @@ class GNS(TalkerSentence, LatLonFix):
         ('Differential reference station ID', 'diferential'),
     )
 
+class GRS(TalkerSentence):
+    """ Order of satellites will match those in the last GSA
+    """
+    fields = (
+        ('Timestamp', 'timestamp', timestamp),
+        ('Residuals mode', 'residuals_mode'),
+        ('SV 01 Residual (m)', 'sv_res_01'),
+        ('SV 02 Residual (m)', 'sv_res_02'),
+        ('SV 03 Residual (m)', 'sv_res_03'),
+        ('SV 04 Residual (m)', 'sv_res_04'),
+        ('SV 05 Residual (m)', 'sv_res_05'),
+        ('SV 06 Residual (m)', 'sv_res_06'),
+        ('SV 07 Residual (m)', 'sv_res_07'),
+        ('SV 08 Residual (m)', 'sv_res_08'),
+        ('SV 09 Residual (m)', 'sv_res_09'),
+        ('SV 10 Residual (m)', 'sv_res_10'),
+        ('SV 11 Residual (m)', 'sv_res_11'),
+        ('SV 12 Residual (m)', 'sv_res_12'),
+    )
+
 class BWW(TalkerSentence):
     """ Bearing, Waypoint to Waypoint
     """
diff --git a/test/test_types.py b/test/test_types.py
index f5dc72d..4c64d5b 100644
--- a/test/test_types.py
+++ b/test/test_types.py
@@ -220,3 +220,17 @@ def test_STALK_unidentified_command():
     assert msg.render() == data
     assert msg.command_name == 'Unknown Command'
 
+def test_GRS():
+    data = "$GNGRS,162047.00,1,0.6,0.1,-16.6,-0.8,-0.1,0.5,,,,,,*41"
+    msg = pynmea2.parse(data)
+    assert msg.render() == data
+    assert msg.talker == 'GN'
+    assert msg.sentence_type == 'GRS'
+    assert msg.residuals_mode == '1'
+    assert msg.sv_res_01 == '0.6'
+    assert msg.sv_res_02 == '0.1'
+    assert msg.sv_res_03 == '-16.6'
+    assert msg.sv_res_04 == '-0.8'
+    assert msg.sv_res_05 == '-0.1'
+    assert msg.sv_res_06 == '0.5'
+    assert msg.sv_res_07 == ''

From 096d2f6db01c845d7e5a59cb5995180a1b453203 Mon Sep 17 00:00:00 2001
From: Tom Flanagan <tom@zkpq.ca>
Date: Sat, 21 Mar 2020 15:29:05 -0700
Subject: [PATCH 12/35] Fix example code

---
 README.md               | 59 ++++++++++++++++++++++++++++++-----------
 examples/data.log       |  5 ++++
 examples/read_file.py   | 11 ++++++++
 examples/read_serial.py | 20 ++++++++++++++
 examples/serial.py      | 32 ----------------------
 5 files changed, 79 insertions(+), 48 deletions(-)
 create mode 100644 examples/data.log
 create mode 100644 examples/read_file.py
 create mode 100644 examples/read_serial.py
 delete mode 100644 examples/serial.py

diff --git a/README.md b/README.md
index 83048ed..14d88e2 100644
--- a/README.md
+++ b/README.md
@@ -7,6 +7,10 @@ pynmea2
 
 The `pynmea2` homepage is located at http://github.com/Knio/pynmea2
 
+ ### Compatibility
+
+`pynmea2` is compatable with Python 2.7 and Python 3.4+
+
 ![Python version](https://img.shields.io/pypi/pyversions/pynmea2.svg?style=flat)
 [![Build status](https://img.shields.io/travis/Knio/pynmea2/master.svg?style=flat)](https://travis-ci.org/Knio/pynmea2)
 [![Coverage status](https://img.shields.io/coveralls/github/Knio/pynmea2/master.svg?style=flat)](https://coveralls.io/r/Knio/pynmea2?branch=master)
@@ -106,28 +110,51 @@ and generate a NMEA string from a `NMEASentence` object:
 '$GPGGA,184353.07,1929.045,S,02410.506,E,1,04,2.6,100.00,M,-33.9,M,,0000*6D'
 ```
 
-Streaming
----------
 
-`pynmea2` can also process streams of NMEA sentences like so, by feeding chunks of data
-manually:
+File reading example
+--------
+
+See [](examples.read_file.py)
 
 ```python
-streamreader = pynmea2.NMEAStreamReader()
-while 1:
-    data = input.read()
-    for msg in streamreader.next(data):
-        print msg
+import pynmea2
+
+file = open('examples/data.log', encoding='utf-8')
+
+for line in file.readlines():
+    try:
+        msg = pynmea2.parse(line)
+        print(repr(msg))
+    except pynmea2.ParseError as e:
+        print('Parse error: {}'.format(e))
+        continue
 ```
 
-or given a file-like device, automatically:
+
+pySerial device example
+---------
+
+See [](examples.read_serial.py)
 
 ```python
-    streamreader = pynmea2.NMEAStreamReader(input)
-    while 1:
-        for msg in streamreader.next():
-            print msg
-```
+import io
 
+import pynmea2
+import serial
 
-If your stream is noisy and contains errors, you can set some basic error handling with the [`errors` parameter of the `NMEAStreamReader` constructor.](pynmea2/stream.py#L12)
+
+ser = serial.Serial('/dev/ttyS1', 9600, timeout=5.0)
+sio = io.TextIOWrapper(io.BufferedRWPair(ser, ser))
+
+while 1:
+    try:
+        line = sio.readline()
+        msg = pynmea2.parse(line)
+        print(repr(msg))
+    except serial.SerialException as e:
+        print('Device error: {}'.format(e))
+        break
+    except pynmea2.ParseError as e:
+        print('Parse error: {}'.format(e))
+        continue
+```
\ No newline at end of file
diff --git a/examples/data.log b/examples/data.log
new file mode 100644
index 0000000..b93f526
--- /dev/null
+++ b/examples/data.log
@@ -0,0 +1,5 @@
+$GPGGA,184353.07,1929.045,S,02410.506,E,1,04,2.6,100.00,M,-33.9,M,,0000*6D
+$GPRTE,2,1,c,0,PBRCPK,PBRTO,PTELGR,PPLAND,PYAMBU,PPFAIR,PWARRN,PMORTL,PLISMR*73
+$GPR00,A,B,C*29
+foobar
+$IIMWV,271.0,R,000.2,N,A*3B
\ No newline at end of file
diff --git a/examples/read_file.py b/examples/read_file.py
new file mode 100644
index 0000000..ebd46ea
--- /dev/null
+++ b/examples/read_file.py
@@ -0,0 +1,11 @@
+import pynmea2
+
+file = open('examples/data.log', encoding='utf-8')
+
+for line in file.readlines():
+    try:
+        msg = pynmea2.parse(line)
+        print(repr(msg))
+    except pynmea2.ParseError as e:
+        print('Parse error: {}'.format(e))
+        continue
diff --git a/examples/read_serial.py b/examples/read_serial.py
new file mode 100644
index 0000000..09bf00e
--- /dev/null
+++ b/examples/read_serial.py
@@ -0,0 +1,20 @@
+import io
+
+import pynmea2
+import serial
+
+
+ser = serial.Serial('/dev/ttyS1', 9600, timeout=5.0)
+sio = io.TextIOWrapper(io.BufferedRWPair(ser, ser))
+
+while 1:
+    try:
+        line = sio.readline()
+        msg = pynmea2.parse(line)
+        print(repr(msg))
+    except serial.SerialException as e:
+        print('Device error: {}'.format(e))
+        break
+    except pynmea2.ParseError as e:
+        print('Parse error: {}'.format(e))
+        continue
\ No newline at end of file
diff --git a/examples/serial.py b/examples/serial.py
deleted file mode 100644
index 8629a24..0000000
--- a/examples/serial.py
+++ /dev/null
@@ -1,32 +0,0 @@
-import time
-import serial
-import pynmea2
-
-
-def read(filename):
-    f = open(filename)
-    reader = pynmea2.NMEAStreamReader(f)
-
-    while 1:
-        for msg in reader.next():
-          print(msg)
-
-
-def read_serial(filename):
-    com = None
-    reader = pynmea2.NMEAStreamReader()
-
-    while 1:
-
-        if com is None:
-          try:
-            com = serial.Serial(filename, timeout=5.0)
-          except serial.SerialException:
-            print('could not connect to %s' % filename)
-            time.sleep(5.0)
-            continue
-
-        data = com.read(16)
-        for msg in reader.next(data):
-          print(msg)
-

From 0fb8e075ffcdc857be72086c8e2914b9494e1715 Mon Sep 17 00:00:00 2001
From: Tom Flanagan <tom@zkpq.ca>
Date: Sat, 21 Mar 2020 15:43:09 -0700
Subject: [PATCH 13/35] Fix readme

---
 .travis.yml | 2 ++
 README.md   | 5 ++---
 setup.py    | 2 ++
 3 files changed, 6 insertions(+), 3 deletions(-)

diff --git a/.travis.yml b/.travis.yml
index be73367..9756596 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -5,6 +5,8 @@ python:
   - "3.4"
   - "3.5"
   - "3.6"
+  - "3.7"
+  - "3.8"
   - "pypy"
   - "pypy3"
 
diff --git a/README.md b/README.md
index 14d88e2..4fa2f34 100644
--- a/README.md
+++ b/README.md
@@ -14,7 +14,6 @@ The `pynmea2` homepage is located at http://github.com/Knio/pynmea2
 ![Python version](https://img.shields.io/pypi/pyversions/pynmea2.svg?style=flat)
 [![Build status](https://img.shields.io/travis/Knio/pynmea2/master.svg?style=flat)](https://travis-ci.org/Knio/pynmea2)
 [![Coverage status](https://img.shields.io/coveralls/github/Knio/pynmea2/master.svg?style=flat)](https://coveralls.io/r/Knio/pynmea2?branch=master)
-[![Code Health](https://landscape.io/github/Knio/pynmea2/master/landscape.svg?style=flat)](https://landscape.io/github/Knio/pynmea2/master)
 
 ### Installation
 
@@ -114,7 +113,7 @@ and generate a NMEA string from a `NMEASentence` object:
 File reading example
 --------
 
-See [](examples.read_file.py)
+See [examples/read_file.py](/examples/read_file.py)
 
 ```python
 import pynmea2
@@ -134,7 +133,7 @@ for line in file.readlines():
 pySerial device example
 ---------
 
-See [](examples.read_serial.py)
+See [examples/read_serial.py](/examples/read_serial.py)
 
 ```python
 import io
diff --git a/setup.py b/setup.py
index dc6b841..836f351 100644
--- a/setup.py
+++ b/setup.py
@@ -26,6 +26,8 @@
         'Programming Language :: Python :: 3.4',
         'Programming Language :: Python :: 3.5',
         'Programming Language :: Python :: 3.6',
+        'Programming Language :: Python :: 3.7',
+        'Programming Language :: Python :: 3.8',
         'Programming Language :: Python :: Implementation :: PyPy',
         'Topic :: Scientific/Engineering :: GIS',
         'Topic :: Software Development :: Libraries :: Python Modules',

From cdc4f37e7dd1465ec37f7baede237e430f31497a Mon Sep 17 00:00:00 2001
From: Tom Flanagan <tom@zkpq.ca>
Date: Sat, 21 Mar 2020 15:29:05 -0700
Subject: [PATCH 14/35] Fix example code

---
 README.md               | 59 ++++++++++++++++++++++++++++++-----------
 examples/data.log       |  5 ++++
 examples/read_file.py   | 11 ++++++++
 examples/read_serial.py | 20 ++++++++++++++
 examples/serial.py      | 32 ----------------------
 5 files changed, 79 insertions(+), 48 deletions(-)
 create mode 100644 examples/data.log
 create mode 100644 examples/read_file.py
 create mode 100644 examples/read_serial.py
 delete mode 100644 examples/serial.py

diff --git a/README.md b/README.md
index 83048ed..14d88e2 100644
--- a/README.md
+++ b/README.md
@@ -7,6 +7,10 @@ pynmea2
 
 The `pynmea2` homepage is located at http://github.com/Knio/pynmea2
 
+ ### Compatibility
+
+`pynmea2` is compatable with Python 2.7 and Python 3.4+
+
 ![Python version](https://img.shields.io/pypi/pyversions/pynmea2.svg?style=flat)
 [![Build status](https://img.shields.io/travis/Knio/pynmea2/master.svg?style=flat)](https://travis-ci.org/Knio/pynmea2)
 [![Coverage status](https://img.shields.io/coveralls/github/Knio/pynmea2/master.svg?style=flat)](https://coveralls.io/r/Knio/pynmea2?branch=master)
@@ -106,28 +110,51 @@ and generate a NMEA string from a `NMEASentence` object:
 '$GPGGA,184353.07,1929.045,S,02410.506,E,1,04,2.6,100.00,M,-33.9,M,,0000*6D'
 ```
 
-Streaming
----------
 
-`pynmea2` can also process streams of NMEA sentences like so, by feeding chunks of data
-manually:
+File reading example
+--------
+
+See [](examples.read_file.py)
 
 ```python
-streamreader = pynmea2.NMEAStreamReader()
-while 1:
-    data = input.read()
-    for msg in streamreader.next(data):
-        print msg
+import pynmea2
+
+file = open('examples/data.log', encoding='utf-8')
+
+for line in file.readlines():
+    try:
+        msg = pynmea2.parse(line)
+        print(repr(msg))
+    except pynmea2.ParseError as e:
+        print('Parse error: {}'.format(e))
+        continue
 ```
 
-or given a file-like device, automatically:
+
+pySerial device example
+---------
+
+See [](examples.read_serial.py)
 
 ```python
-    streamreader = pynmea2.NMEAStreamReader(input)
-    while 1:
-        for msg in streamreader.next():
-            print msg
-```
+import io
 
+import pynmea2
+import serial
 
-If your stream is noisy and contains errors, you can set some basic error handling with the [`errors` parameter of the `NMEAStreamReader` constructor.](pynmea2/stream.py#L12)
+
+ser = serial.Serial('/dev/ttyS1', 9600, timeout=5.0)
+sio = io.TextIOWrapper(io.BufferedRWPair(ser, ser))
+
+while 1:
+    try:
+        line = sio.readline()
+        msg = pynmea2.parse(line)
+        print(repr(msg))
+    except serial.SerialException as e:
+        print('Device error: {}'.format(e))
+        break
+    except pynmea2.ParseError as e:
+        print('Parse error: {}'.format(e))
+        continue
+```
\ No newline at end of file
diff --git a/examples/data.log b/examples/data.log
new file mode 100644
index 0000000..b93f526
--- /dev/null
+++ b/examples/data.log
@@ -0,0 +1,5 @@
+$GPGGA,184353.07,1929.045,S,02410.506,E,1,04,2.6,100.00,M,-33.9,M,,0000*6D
+$GPRTE,2,1,c,0,PBRCPK,PBRTO,PTELGR,PPLAND,PYAMBU,PPFAIR,PWARRN,PMORTL,PLISMR*73
+$GPR00,A,B,C*29
+foobar
+$IIMWV,271.0,R,000.2,N,A*3B
\ No newline at end of file
diff --git a/examples/read_file.py b/examples/read_file.py
new file mode 100644
index 0000000..ebd46ea
--- /dev/null
+++ b/examples/read_file.py
@@ -0,0 +1,11 @@
+import pynmea2
+
+file = open('examples/data.log', encoding='utf-8')
+
+for line in file.readlines():
+    try:
+        msg = pynmea2.parse(line)
+        print(repr(msg))
+    except pynmea2.ParseError as e:
+        print('Parse error: {}'.format(e))
+        continue
diff --git a/examples/read_serial.py b/examples/read_serial.py
new file mode 100644
index 0000000..09bf00e
--- /dev/null
+++ b/examples/read_serial.py
@@ -0,0 +1,20 @@
+import io
+
+import pynmea2
+import serial
+
+
+ser = serial.Serial('/dev/ttyS1', 9600, timeout=5.0)
+sio = io.TextIOWrapper(io.BufferedRWPair(ser, ser))
+
+while 1:
+    try:
+        line = sio.readline()
+        msg = pynmea2.parse(line)
+        print(repr(msg))
+    except serial.SerialException as e:
+        print('Device error: {}'.format(e))
+        break
+    except pynmea2.ParseError as e:
+        print('Parse error: {}'.format(e))
+        continue
\ No newline at end of file
diff --git a/examples/serial.py b/examples/serial.py
deleted file mode 100644
index 8629a24..0000000
--- a/examples/serial.py
+++ /dev/null
@@ -1,32 +0,0 @@
-import time
-import serial
-import pynmea2
-
-
-def read(filename):
-    f = open(filename)
-    reader = pynmea2.NMEAStreamReader(f)
-
-    while 1:
-        for msg in reader.next():
-          print(msg)
-
-
-def read_serial(filename):
-    com = None
-    reader = pynmea2.NMEAStreamReader()
-
-    while 1:
-
-        if com is None:
-          try:
-            com = serial.Serial(filename, timeout=5.0)
-          except serial.SerialException:
-            print('could not connect to %s' % filename)
-            time.sleep(5.0)
-            continue
-
-        data = com.read(16)
-        for msg in reader.next(data):
-          print(msg)
-

From 4a4394a596e1ed1c2737acf0c22e7f9f3f77b0e4 Mon Sep 17 00:00:00 2001
From: Tom Flanagan <tom@zkpq.ca>
Date: Sat, 21 Mar 2020 15:43:09 -0700
Subject: [PATCH 15/35] Fix readme

---
 .travis.yml | 2 ++
 README.md   | 5 ++---
 setup.py    | 2 ++
 3 files changed, 6 insertions(+), 3 deletions(-)

diff --git a/.travis.yml b/.travis.yml
index be73367..9756596 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -5,6 +5,8 @@ python:
   - "3.4"
   - "3.5"
   - "3.6"
+  - "3.7"
+  - "3.8"
   - "pypy"
   - "pypy3"
 
diff --git a/README.md b/README.md
index 14d88e2..4fa2f34 100644
--- a/README.md
+++ b/README.md
@@ -14,7 +14,6 @@ The `pynmea2` homepage is located at http://github.com/Knio/pynmea2
 ![Python version](https://img.shields.io/pypi/pyversions/pynmea2.svg?style=flat)
 [![Build status](https://img.shields.io/travis/Knio/pynmea2/master.svg?style=flat)](https://travis-ci.org/Knio/pynmea2)
 [![Coverage status](https://img.shields.io/coveralls/github/Knio/pynmea2/master.svg?style=flat)](https://coveralls.io/r/Knio/pynmea2?branch=master)
-[![Code Health](https://landscape.io/github/Knio/pynmea2/master/landscape.svg?style=flat)](https://landscape.io/github/Knio/pynmea2/master)
 
 ### Installation
 
@@ -114,7 +113,7 @@ and generate a NMEA string from a `NMEASentence` object:
 File reading example
 --------
 
-See [](examples.read_file.py)
+See [examples/read_file.py](/examples/read_file.py)
 
 ```python
 import pynmea2
@@ -134,7 +133,7 @@ for line in file.readlines():
 pySerial device example
 ---------
 
-See [](examples.read_serial.py)
+See [examples/read_serial.py](/examples/read_serial.py)
 
 ```python
 import io
diff --git a/setup.py b/setup.py
index dc6b841..836f351 100644
--- a/setup.py
+++ b/setup.py
@@ -26,6 +26,8 @@
         'Programming Language :: Python :: 3.4',
         'Programming Language :: Python :: 3.5',
         'Programming Language :: Python :: 3.6',
+        'Programming Language :: Python :: 3.7',
+        'Programming Language :: Python :: 3.8',
         'Programming Language :: Python :: Implementation :: PyPy',
         'Topic :: Scientific/Engineering :: GIS',
         'Topic :: Software Development :: Libraries :: Python Modules',

From 2aa97fa1d1cf98e2a86c13df13e46e3f21579d7e Mon Sep 17 00:00:00 2001
From: Tom Flanagan <tom@zkpq.ca>
Date: Sat, 21 Mar 2020 16:14:40 -0700
Subject: [PATCH 16/35] Add types

---
 pynmea2/types/talker.py | 26 +++++++++++++-------------
 test/test_types.py      | 16 ++++++++--------
 2 files changed, 21 insertions(+), 21 deletions(-)

diff --git a/pynmea2/types/talker.py b/pynmea2/types/talker.py
index 74b1d34..547d003 100644
--- a/pynmea2/types/talker.py
+++ b/pynmea2/types/talker.py
@@ -186,19 +186,19 @@ class GRS(TalkerSentence):
     """
     fields = (
         ('Timestamp', 'timestamp', timestamp),
-        ('Residuals mode', 'residuals_mode'),
-        ('SV 01 Residual (m)', 'sv_res_01'),
-        ('SV 02 Residual (m)', 'sv_res_02'),
-        ('SV 03 Residual (m)', 'sv_res_03'),
-        ('SV 04 Residual (m)', 'sv_res_04'),
-        ('SV 05 Residual (m)', 'sv_res_05'),
-        ('SV 06 Residual (m)', 'sv_res_06'),
-        ('SV 07 Residual (m)', 'sv_res_07'),
-        ('SV 08 Residual (m)', 'sv_res_08'),
-        ('SV 09 Residual (m)', 'sv_res_09'),
-        ('SV 10 Residual (m)', 'sv_res_10'),
-        ('SV 11 Residual (m)', 'sv_res_11'),
-        ('SV 12 Residual (m)', 'sv_res_12'),
+        ('Residuals mode', 'residuals_mode', int),
+        ('SV 01 Residual (m)', 'sv_res_01', float),
+        ('SV 02 Residual (m)', 'sv_res_02', float),
+        ('SV 03 Residual (m)', 'sv_res_03', float),
+        ('SV 04 Residual (m)', 'sv_res_04', float),
+        ('SV 05 Residual (m)', 'sv_res_05', float),
+        ('SV 06 Residual (m)', 'sv_res_06', float),
+        ('SV 07 Residual (m)', 'sv_res_07', float),
+        ('SV 08 Residual (m)', 'sv_res_08', float),
+        ('SV 09 Residual (m)', 'sv_res_09', float),
+        ('SV 10 Residual (m)', 'sv_res_10', float),
+        ('SV 11 Residual (m)', 'sv_res_11', float),
+        ('SV 12 Residual (m)', 'sv_res_12', float),
     )
 
 class BWW(TalkerSentence):
diff --git a/test/test_types.py b/test/test_types.py
index 4c64d5b..759227e 100644
--- a/test/test_types.py
+++ b/test/test_types.py
@@ -226,11 +226,11 @@ def test_GRS():
     assert msg.render() == data
     assert msg.talker == 'GN'
     assert msg.sentence_type == 'GRS'
-    assert msg.residuals_mode == '1'
-    assert msg.sv_res_01 == '0.6'
-    assert msg.sv_res_02 == '0.1'
-    assert msg.sv_res_03 == '-16.6'
-    assert msg.sv_res_04 == '-0.8'
-    assert msg.sv_res_05 == '-0.1'
-    assert msg.sv_res_06 == '0.5'
-    assert msg.sv_res_07 == ''
+    assert msg.residuals_mode == 1
+    assert msg.sv_res_01 == 0.6
+    assert msg.sv_res_02 == 0.1
+    assert msg.sv_res_03 == -16.6
+    assert msg.sv_res_04 == -0.8
+    assert msg.sv_res_05 == -0.1
+    assert msg.sv_res_06 == 0.5
+    assert msg.sv_res_07 == None

From e77304e4080c02705f13ef42594a00b4c74e892d Mon Sep 17 00:00:00 2001
From: KD7TKJ <tyrell@jentink.net>
Date: Wed, 18 Mar 2020 21:25:36 -0700
Subject: [PATCH 17/35] Add support for Garmin, Magellan, and Kenwood
 proprietary waypoint locations.

---
 pynmea2/nmea_utils.py                 |   6 +-
 pynmea2/types/proprietary/__init__.py |   2 +
 pynmea2/types/proprietary/grm.py      |  32 ++++++--
 pynmea2/types/proprietary/kwd.py      | 104 ++++++++++++++++++++++++++
 pynmea2/types/proprietary/mgn.py      |  52 +++++++++++++
 test/test_proprietary.py              |  52 +++++++++++++
 6 files changed, 242 insertions(+), 6 deletions(-)
 create mode 100644 pynmea2/types/proprietary/kwd.py
 create mode 100644 pynmea2/types/proprietary/mgn.py

diff --git a/pynmea2/nmea_utils.py b/pynmea2/nmea_utils.py
index fb2b060..9f1bf45 100644
--- a/pynmea2/nmea_utils.py
+++ b/pynmea2/nmea_utils.py
@@ -1,5 +1,10 @@
 #pylint: disable=invalid-name
 import datetime
+import re
+
+def valid(s):
+    return s == 'A'
+
 
 def timestamp(s):
     '''
@@ -25,7 +30,6 @@ def datestamp(s):
     return datetime.datetime.strptime(s, '%d%m%y').date()
 
 
-import re
 def dm_to_sd(dm):
     '''
     Converts a geographic co-ordinate given in "degrees/minutes" dddmm.mmmm
diff --git a/pynmea2/types/proprietary/__init__.py b/pynmea2/types/proprietary/__init__.py
index 540ab50..3aae55a 100644
--- a/pynmea2/types/proprietary/__init__.py
+++ b/pynmea2/types/proprietary/__init__.py
@@ -1,5 +1,7 @@
 from . import ash
 from . import grm
+from . import kwd
+from . import mgn
 from . import rdi
 from . import srf
 from . import sxn
diff --git a/pynmea2/types/proprietary/grm.py b/pynmea2/types/proprietary/grm.py
index bd925a6..be8af19 100644
--- a/pynmea2/types/proprietary/grm.py
+++ b/pynmea2/types/proprietary/grm.py
@@ -27,7 +27,7 @@ class GRME(GRM):
         ("Estimated Vert. Position Error", "vpe", Decimal),
         ("Estimated Vert. Position Error Unit (M)", "vpe_unit"),
         ("Estimated Horiz. Position Error", "osepe", Decimal),
-        ("Overall Spherical Equiv. Position Error", "osepe_unit")
+        ("Overall Spherical Equiv. Position Error", "osepe_unit"),
     )
 
 
@@ -40,6 +40,31 @@ class GRMM(GRM):
     )
 
 
+class GRMW(GRM):
+    """ GARMIN Waypoint Information
+
+    https://www8.garmin.com/support/pdf/NMEA_0183.pdf
+    https://github.com/wb2osz/direwolf/blob/master/waypoint.c
+
+    $PGRMW,wname,alt,symbol,comment*99
+    Where,
+       wname		is waypoint name.  Must match existing waypoint.
+       alt		is altitude in meters.
+       symbol		is symbol code.  Hexadecimal up to FFFF.
+                    See Garmin Device Interface Specification
+                    001-0063-00 for values of "symbol_type."
+       comment      is comment for the waypoint.
+       *99		    is checksum
+    """
+    fields = (
+        ("Subtype", "subtype"),
+        ("Waypoint Name", "wname"),
+        ("Altitude", "altitude", Decimal),
+        ("Symbol", "symbol"),
+        ("Comment", "comment"),
+    )
+
+
 class GRMZ(GRM):
     """ GARMIN Altitude Information
     """
@@ -47,8 +72,5 @@ class GRMZ(GRM):
         ("Subtype", "subtype"),
         ("Altitude", "altitude", Decimal),
         ("Altitude Units (Feet)", "altitude_unit"),
-        ("Positional Fix Dimension (2=user, 3=GPS)", "pos_fix_dim")
+        ("Positional Fix Dimension (2=user, 3=GPS)", "pos_fix_dim"),
     )
-
-
-
diff --git a/pynmea2/types/proprietary/kwd.py b/pynmea2/types/proprietary/kwd.py
new file mode 100644
index 0000000..ca80945
--- /dev/null
+++ b/pynmea2/types/proprietary/kwd.py
@@ -0,0 +1,104 @@
+# Kenwood
+
+from decimal import Decimal
+from datetime import date, time
+
+from ... import nmea
+from ... import nmea_utils
+
+
+class KWD(nmea.ProprietarySentence):
+    sentence_types = {}
+
+    def __new__(_cls, manufacturer, data):
+        name = manufacturer + data[0]
+        cls = _cls.sentence_types.get(name, _cls)
+        return super(KWD, cls).__new__(cls)
+
+    def __init__(self, manufacturer, data):
+        self.sentence_type = manufacturer + data[0]
+        super(KWD, self).__init__(manufacturer, data)
+
+
+class KWDWPL(KWD, nmea_utils.LatLonFix, nmea_utils.DatetimeFix, nmea_utils.ValidStatusFix):
+    """ Kenwood Waypoint Location
+
+    https://github.com/wb2osz/direwolf/blob/master/waypoint.c
+
+    $PKWDWPL,hhmmss,v,ddmm.mm,ns,dddmm.mm,ew,speed,course,ddmmyy,alt,wname,ts*99
+    Where,
+       hhmmss       is time in UTC from the clock in the transceiver.
+                    This will be bogus if the clock was not set properly.
+                    It does not use the timestamp from a position
+                    report which could be useful.
+
+       GPS Status	A = active, V = void.
+                    It looks like this might be modeled after the GPS status values
+                    we see in $GPRMC.  i.e. Does the transceiver know its location?
+                    I don't see how that information would be relevant in this context.
+                    I've observed this under various conditions (No GPS, GPS with/without
+                    fix) and it has always been "V."
+
+       ddmm.mm,ns   is latitude. N or S.
+       dddmm.mm,ew  is longitude.  E or W.
+       speed        is speed over ground, knots.
+       course       is course over ground, degrees.
+       ddmmyy       is date.  See comments for time.
+       alt          is altitude, meters above mean sea level.
+       wname        is the waypoint name.  For an Object Report, the id is the object name.
+                    For a position report, it is the call of the sending station.
+                    An Object name can contain any printable characters.
+                    What if object name contains , or * characters?
+                    Those are field delimiter characters and it would be unfortunate
+                    if they appeared in a NMEA sentence data field.
+
+                    If there is a comma in the name, such as "test,5" the Kenwood TM-D710A displays
+                    it fine but we end up with an extra field.
+
+                       $PKWDWPL,150803,V,4237.14,N,07120.83,W,,,190316,,test,5,/'*30
+
+                    If the name contains an asterisk, it doesn't show up on the
+                    display and no waypoint sentence is generated.
+                    Some other talkers substitute these two characters following the AvMap precedent.
+
+                       $PKWDWPL,204714,V,4237.1400,N,07120.8300,W,,,200316,,test|5,/'*61
+                       $PKWDWPL,204719,V,4237.1400,N,07120.8300,W,,,200316,,test~6,/'*6D
+
+       ts           are the table and symbol.
+
+                    What happens if the symbol is comma or asterisk?
+                        , Boy Scouts / Girl Scouts
+                        * SnowMobile / Snow
+
+                    the D710A just pushes them thru without checking.
+                    These would not be parsed properly:
+
+                        $PKWDWPL,150753,V,4237.14,N,07120.83,W,,,190316,,test3,/,*1B
+                        $PKWDWPL,150758,V,4237.14,N,07120.83,W,,,190316,,test4,/ **3B
+
+                    Other talkers do the usual substitution and the other end would
+                    need to change them back after extracting from NMEA sentence.
+
+                       $PKWDWPL,204704,V,4237.1400,N,07120.8300,W,,,200316,,test3,/|*41
+                       $PKWDWPL,204709,V,4237.1400,N,07120.8300,W,,,200316,,test4,/~*49
+
+
+        *99            is checksum
+
+    Oddly, there is no place for comment.
+    """
+    fields = (
+        ("Subtype", "subtype"),
+        ("Time of Receipt", "timestamp", nmea_utils.timestamp),
+        ("GPS Status (Void)","status"),
+        ("Latitude", "lat"),
+        ("Latitude Direction", "lat_dir"),
+        ("Longitude", "lon"),
+        ("Longitude Direction", "lon_dir"),
+        ("Speed over Ground", "sog", float),
+        ("Course over Ground", "cog", float),
+        ("Date", "datestamp", nmea_utils.datestamp),
+        ("Altitude", "altitude", Decimal),
+        ("Waypoint Name", "wname"),
+        ("Table and Symbol", "ts"),
+    )
diff --git a/pynmea2/types/proprietary/mgn.py b/pynmea2/types/proprietary/mgn.py
new file mode 100644
index 0000000..a2dc7df
--- /dev/null
+++ b/pynmea2/types/proprietary/mgn.py
@@ -0,0 +1,52 @@
+# Magellan
+
+from decimal import Decimal
+
+from ... import nmea
+from ... import nmea_utils
+
+
+class MGN(nmea.ProprietarySentence):
+    sentence_types = {}
+
+    def __new__(_cls, manufacturer, data):
+        name = manufacturer + data[0]
+        cls = _cls.sentence_types.get(name, _cls)
+        return super(MGN, cls).__new__(cls)
+
+    def __init__(self, manufacturer, data):
+        self.sentence_type = manufacturer + data[0]
+        super(MGN, self).__init__(manufacturer, data)
+
+
+class MGNWPL(MGN, nmea_utils.LatLonFix):
+    """ Magellan Waypoint Location
+
+    https://github.com/wb2osz/direwolf/blob/master/waypoint.c
+
+    $PMGNWPL,ddmm.mmmm,ns,dddmm.mmmm,ew,alt,unit,wname,comment,icon,xx*99
+    Where,
+        ddmm.mmmm,ns    is latitude
+        dddmm.mmmm,ew   is longitude
+        alt             is altitude
+        unit            is M for meters or F for feet
+        wname           is the waypoint name
+        comment         is message or comment
+        icon            is one or two letters for icon code
+        xx              is waypoint type which is optional, not well
+                        defined, and not used in their example.
+        *99             is checksum
+    """
+    fields = (
+        ("Subtype", "subtype"),
+        ("Latitude", "lat"),
+        ("Latitude Direction", "lat_dir"),
+        ("Longitude", "lon"),
+        ("Longitude Direction", "lon_dir"),
+        ("Altitude", "altitude", Decimal),
+        ("Altitude Units (Feet/Meters)", "altitude_unit"),
+        ("Waypoint Name", "wname"),
+        ("Comment", "comment"),
+        ("Icon", "icon"),
+        ("Waypoint Type", "type")
+    )
diff --git a/test/test_proprietary.py b/test/test_proprietary.py
index 5522db9..f99972d 100644
--- a/test/test_proprietary.py
+++ b/test/test_proprietary.py
@@ -119,6 +119,7 @@ def test_grm():
     assert msg.osepe == 25.0
     assert msg.osepe_unit == 'M'
 
+
 def test_tnl():
     data = '$PTNL,BPQ,224445.06,021207,3723.09383914,N,12200.32620132,W,EHT-5.923,M,5*60'
     msg = pynmea2.parse(data)
@@ -195,3 +196,54 @@ def test_proprietary_VTX_0012():
     assert msg.gain == 38.8
     assert msg.render() == data
 
+
+def test_proprietary_GRMW():
+    # A sample proprietary Garmin Waypoint sentence, generated by DIREWOLF
+    data = "$PGRMW,AC7FD-1,,000A,AC7FD local DIGI U=12.5V|T=23.9C*1A"
+    msg = pynmea2.parse(data)
+    assert msg.manufacturer == 'GRM'
+    assert msg.wname == 'AC7FD-1'
+    assert msg.altitude == None
+    assert msg.symbol == '000A'
+    assert msg.comment == 'AC7FD local DIGI U=12.5V|T=23.9C'
+
+
+def test_proprietary_MGNWPL():
+    # A sample proprietary Magellan Waypoint sentence, generated by DIREWOLF
+    data = "$PMGNWPL,4531.7900,N,12253.4800,W,,M,AC7FD-1,AC7FD local DIGI U=12.5V|T=23.9C,c*46"
+    msg = pynmea2.parse(data)
+    assert msg.manufacturer == 'MGN'
+    assert msg.lat =='4531.7900'
+    assert msg.lat_dir == 'N'
+    assert msg.lon == '12253.4800'
+    assert msg.lon_dir == 'W'
+    assert msg.altitude == None
+    assert msg.altitude_unit == 'M'
+    assert msg.wname == 'AC7FD-1'
+    assert msg.comment == 'AC7FD local DIGI U=12.5V|T=23.9C'
+    assert msg.icon == 'c'
+    assert msg.latitude == 45.529833333333336
+    assert msg.longitude == -122.89133333333334
+
+
+def test_KWDWPL():
+    # A sample proprietary Kenwood Waypoint sentence, generated by DIREWOLF
+    data = "$PKWDWPL,053125,V,4531.7900,N,12253.4800,W,,,200320,,AC7FD-1,/-*10"
+    msg = pynmea2.parse(data)
+    assert msg.manufacturer == "KWD"
+    assert msg.timestamp == datetime.time(5, 31, 25)
+    assert msg.status == 'V'
+    assert msg.is_valid == False
+    assert msg.lat == '4531.7900'
+    assert msg.lat_dir == 'N'
+    assert msg.lon == '12253.4800'
+    assert msg.lon_dir == 'W'
+    assert msg.sog == None
+    assert msg.cog == None
+    assert msg.datestamp == datetime.date(2020, 3, 20)
+    assert msg.datetime == datetime.datetime(2020, 3, 20, 5, 31, 25)
+    assert msg.altitude == None
+    assert msg.wname == 'AC7FD-1'
+    assert msg.ts == '/-'
+    assert msg.latitude == 45.529833333333336
+    assert msg.longitude == -122.89133333333334

From 3ab851692700b56862c8cf3cec204f93f638e147 Mon Sep 17 00:00:00 2001
From: Tom Flanagan <tom@zkpq.ca>
Date: Sat, 20 Feb 2021 14:14:19 -0800
Subject: [PATCH 18/35] Remove trailing comma from QuerySentence identifier.
 Fixes #120

---
 pynmea2/_version.py | 2 +-
 pynmea2/nmea.py     | 2 +-
 test/test_pynmea.py | 7 ++++++-
 3 files changed, 8 insertions(+), 3 deletions(-)

diff --git a/pynmea2/_version.py b/pynmea2/_version.py
index 6819333..ce51117 100644
--- a/pynmea2/_version.py
+++ b/pynmea2/_version.py
@@ -1 +1 @@
-__version__ = '1.15.0'
+__version__ = '1.16.0'
diff --git a/pynmea2/nmea.py b/pynmea2/nmea.py
index b9f5f2e..a7c13d0 100644
--- a/pynmea2/nmea.py
+++ b/pynmea2/nmea.py
@@ -232,7 +232,7 @@ def __init__(self, talker, listener, sentence_type):
         self.data = []
 
     def identifier(self):
-        return '%s%sQ,%s,' % (self.talker, self.listener, self.sentence_type)
+        return '%s%sQ,%s' % (self.talker, self.listener, self.sentence_type)
 
 
 class ProprietarySentence(NMEASentence):
diff --git a/test/test_pynmea.py b/test/test_pynmea.py
index cdcd4cf..2723b0c 100644
--- a/test/test_pynmea.py
+++ b/test/test_pynmea.py
@@ -5,7 +5,7 @@
 
 
 def test_version():
-    version = '1.15.0'
+    version = '1.16.0'
     assert pynmea2.version == version
     assert pynmea2.__version__ == version
 
@@ -87,12 +87,14 @@ def test_nmea_util():
     assert pynmea2.nmea_utils.dm_to_sd('0') == 0.
     assert pynmea2.nmea_utils.dm_to_sd('12108.1') == 121.135
 
+
 def test_missing_latlon():
     data = '$GPGGA,201716.684,,,,,0,00,,,M,0.0,M,,0000*5F'
     msg = pynmea2.parse(data)
     print(msg)
     assert msg.latitude == 0.
 
+
 def test_query():
     data = 'CCGPQ,GGA'
     msg = pynmea2.parse(data)
@@ -100,6 +102,9 @@ def test_query():
     assert msg.talker == 'CC'
     assert msg.listener == 'GP'
     assert msg.sentence_type == 'GGA'
+    msg = pynmea2.QuerySentence('CC', 'GP', 'GGA')
+    assert msg.render() == '$CCGPQ,GGA*2B'
+
 
 def test_slash():
     with pytest.raises(pynmea2.nmea.ParseError):

From 7109b7d87b2c7c94be41c6c2b37cd2964f8b5b8a Mon Sep 17 00:00:00 2001
From: Tom Flanagan <tom@zkpq.ca>
Date: Sat, 20 Feb 2021 14:32:36 -0800
Subject: [PATCH 19/35] Disable pylint

---
 .travis.yml | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/.travis.yml b/.travis.yml
index 9756596..bbeb6a1 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -14,14 +14,14 @@ python:
 install:
     - pip install pip --upgrade
     - pip install pytest --upgrade
-    - pip install pylint --upgrade
+    # - pip install pylint --upgrade
 
 # command to run tests, e.g. python setup.py test
 script:
     - python setup.py sdist --format=zip
     - pip install dist/pynmea2*.zip
     - py.test
-    - pylint -E pynmea2
+    # - pylint -E pynmea2 ## pylint is not backwards compatible with itself
 
 after_success:
     - pip install coveralls coverage

From d9064ea06c7a7ef094b4e26abcc86bbe2c62cc83 Mon Sep 17 00:00:00 2001
From: Tom Flanagan <tom@zkpq.ca>
Date: Sat, 20 Feb 2021 15:16:29 -0800
Subject: [PATCH 20/35] Add Python 3.9

---
 .travis.yml | 1 +
 setup.py    | 1 +
 2 files changed, 2 insertions(+)

diff --git a/.travis.yml b/.travis.yml
index bbeb6a1..b7e5540 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -7,6 +7,7 @@ python:
   - "3.6"
   - "3.7"
   - "3.8"
+  - "3.9"
   - "pypy"
   - "pypy3"
 
diff --git a/setup.py b/setup.py
index 836f351..facd391 100644
--- a/setup.py
+++ b/setup.py
@@ -28,6 +28,7 @@
         'Programming Language :: Python :: 3.6',
         'Programming Language :: Python :: 3.7',
         'Programming Language :: Python :: 3.8',
+        'Programming Language :: Python :: 3.9',
         'Programming Language :: Python :: Implementation :: PyPy',
         'Topic :: Scientific/Engineering :: GIS',
         'Topic :: Software Development :: Libraries :: Python Modules',

From 2dab8f59045365463a33013cd1f95140943193fd Mon Sep 17 00:00:00 2001
From: Tom Flanagan <tom@zkpq.ca>
Date: Sun, 21 Mar 2021 22:38:07 -0700
Subject: [PATCH 21/35] Fix #121 UBX rendering

---
 pynmea2/_version.py              |  2 +-
 pynmea2/types/proprietary/ubx.py | 11 ++++++-----
 test/test_proprietary.py         | 10 +++++++++-
 test/test_pynmea.py              |  2 +-
 4 files changed, 17 insertions(+), 8 deletions(-)

diff --git a/pynmea2/_version.py b/pynmea2/_version.py
index ce51117..5fe69a4 100644
--- a/pynmea2/_version.py
+++ b/pynmea2/_version.py
@@ -1 +1 @@
-__version__ = '1.16.0'
+__version__ = '1.17.0'
diff --git a/pynmea2/types/proprietary/ubx.py b/pynmea2/types/proprietary/ubx.py
index f9743af..7df0e26 100644
--- a/pynmea2/types/proprietary/ubx.py
+++ b/pynmea2/types/proprietary/ubx.py
@@ -14,15 +14,13 @@ def __new__(_cls, manufacturer, data):
         cls = _cls.sentence_types.get(name, _cls)
         return super(UBX, cls).__new__(cls)
 
-    def __init__(self, manufacturer, data):
-        self.sentence_type = manufacturer + data[1]
-        super(UBX, self).__init__(manufacturer, data[2:])
-
 
 class UBX00(UBX, LatLonFix):
     """ Lat/Long Position Data
     """
     fields = (
+        ("Blank", "_blank"),
+        ("UBX Type", "ubx_type"),
         ("Timestamp (UTC)", "timestamp", timestamp),
         ("Latitude", "lat"),
         ("Latitude Direction", "lat_dir"),
@@ -41,7 +39,6 @@ class UBX00(UBX, LatLonFix):
         ("Time Dilution of Precision", "tdop"),
         ("Number of Satellites Used", "num_svs"),
         ("Reserved", "reserved")
-
     )
 
 
@@ -49,6 +46,8 @@ class UBX03(UBX):
     """ Satellite Status
     """
     fields = (
+        ("Blank", "_blank"),
+        ("UBX Type", "ubx_type"),
         ("Number of GNSS Satellites Tracked", "num_sv", int),
     )
 
@@ -61,6 +60,8 @@ class UBX04(UBX):
     """ Time and Day Clock Information
     """
     fields = (
+        ("Blank", "_blank"),
+        ("UBX Type", "ubx_type"),
         ("UTC Time", "time", timestamp),
         ("UTC Date", "date", datestamp),
         ("UTC Time of Week", "utc_tow"),
diff --git a/test/test_proprietary.py b/test/test_proprietary.py
index f99972d..3e6a526 100644
--- a/test/test_proprietary.py
+++ b/test/test_proprietary.py
@@ -105,10 +105,11 @@ def test_srf():
     data = '$PSRF999,0,1200,8,1,1'
     msg = pynmea2.parse(data)
     assert type(msg) == pynmea2.srf.SRF
+    assert msg.render(checksum=False) == data
 
 
 def test_grm():
-    data = ' $PGRME,15.0,M,45.0,M,25.0,M*1C'
+    data = '$PGRME,15.0,M,45.0,M,25.0,M*1C'
     msg = pynmea2.parse(data)
     assert type(msg) == pynmea2.grm.GRME
     assert msg.sentence_type == 'GRME'
@@ -118,6 +119,7 @@ def test_grm():
     assert msg.vpe_unit == 'M'
     assert msg.osepe == 25.0
     assert msg.osepe_unit == 'M'
+    assert msg.render() == data
 
 
 def test_tnl():
@@ -127,15 +129,19 @@ def test_tnl():
     assert msg.datestamp == datetime.date(2007,12,2)
     assert msg.latitude == 37.384897319
     assert msg.longitude == -122.00543668866666
+    assert msg.render() == data
 
 
 def test_ubx00():
     data = '$PUBX,00,074440.00,4703.74203,N,00736.82976,E,576.991,D3,2.0,2.0,0.091,0.00,-0.032,,0.76,1.05,0.65,14,0,0*70'
     msg = pynmea2.parse(data)
     assert type(msg) == pynmea2.ubx.UBX00
+    assert msg.identifier() == 'PUBX'
+    assert msg.ubx_type == '00'
     assert msg.timestamp == datetime.time(7, 44, 40)
     assert msg.latitude == 47.06236716666667
     assert msg.lat_dir == 'N'
+    assert msg.render() == data
 
 
 def test_ubx03():
@@ -143,6 +149,7 @@ def test_ubx03():
     msg = pynmea2.parse(data)
     assert type(msg) == pynmea2.ubx.UBX03
     assert msg.num_sv == 20
+    assert msg.render() == data
 
 
 def test_ubx04():
@@ -152,6 +159,7 @@ def test_ubx04():
     assert msg.date == datetime.date(2014, 10, 13)
     assert msg.time == datetime.time(7, 38, 24)
     assert msg.clk_bias == 495176
+    assert msg.render() == data
 
 
 def test_create():
diff --git a/test/test_pynmea.py b/test/test_pynmea.py
index 2723b0c..5379e20 100644
--- a/test/test_pynmea.py
+++ b/test/test_pynmea.py
@@ -5,7 +5,7 @@
 
 
 def test_version():
-    version = '1.16.0'
+    version = '1.17.0'
     assert pynmea2.version == version
     assert pynmea2.__version__ == version
 

From e2dd9e5716d144dd24161b5622622fcf9be7e6b1 Mon Sep 17 00:00:00 2001
From: freol35241 <freol@outlook.com>
Date: Thu, 1 Apr 2021 00:12:36 +0200
Subject: [PATCH 22/35] Fixing regex and adding a test (#124)

---
 pynmea2/types/proprietary/ash.py | 2 +-
 test/test_ash.py                 | 7 +++++++
 2 files changed, 8 insertions(+), 1 deletion(-)

diff --git a/pynmea2/types/proprietary/ash.py b/pynmea2/types/proprietary/ash.py
index d62a10e..0715bef 100644
--- a/pynmea2/types/proprietary/ash.py
+++ b/pynmea2/types/proprietary/ash.py
@@ -34,7 +34,7 @@ class ASHRATT(ASH):
     '''
     @staticmethod
     def match(data):
-        return re.match(r'^\d{6}\.\d{3}$', data[1])
+        return re.match(r'^\d{6}\.\d{2,3}$', data[1])
 
     def __init__(self, *args, **kwargs):
         self.subtype = 'ATT'
diff --git a/test/test_ash.py b/test/test_ash.py
index b33261e..24a2bac 100644
--- a/test/test_ash.py
+++ b/test/test_ash.py
@@ -32,6 +32,13 @@ def test_ashratt():
     assert msg.render() == data
 
 
+def test_ashratt_with_2_vs_3_decimal_timestamp():
+    msg_3 = pynmea2.parse('$PASHR,130533.620,0.311,T,-80.467,-1.395,,0.066,0.067,0.215,2,3*0B')
+    msg_2 = pynmea2.parse('$PASHR,130533.62,0.311,T,-80.467,-1.395,,0.066,0.067,0.215,2,3*3B')
+
+    assert msg_3.timestamp == msg_2.timestamp
+
+
 def test_ash_undefined():
     '''
     Test that non-ATT messages still fall back to the generic ASH type

From 37edca47a729d7b403ceca93861908d30b8793d6 Mon Sep 17 00:00:00 2001
From: Tom Flanagan <tom@zkpq.ca>
Date: Sun, 11 Apr 2021 13:48:45 -0700
Subject: [PATCH 23/35] Update travis badge

---
 README.md | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/README.md b/README.md
index 4fa2f34..310fcb2 100644
--- a/README.md
+++ b/README.md
@@ -12,7 +12,7 @@ The `pynmea2` homepage is located at http://github.com/Knio/pynmea2
 `pynmea2` is compatable with Python 2.7 and Python 3.4+
 
 ![Python version](https://img.shields.io/pypi/pyversions/pynmea2.svg?style=flat)
-[![Build status](https://img.shields.io/travis/Knio/pynmea2/master.svg?style=flat)](https://travis-ci.org/Knio/pynmea2)
+[![Build Status](https://www.travis-ci.com/Knio/pynmea2.svg?branch=master)](https://www.travis-ci.com/Knio/pynmea2)
 [![Coverage status](https://img.shields.io/coveralls/github/Knio/pynmea2/master.svg?style=flat)](https://coveralls.io/r/Knio/pynmea2?branch=master)
 
 ### Installation
@@ -156,4 +156,4 @@ while 1:
     except pynmea2.ParseError as e:
         print('Parse error: {}'.format(e))
         continue
-```
\ No newline at end of file
+```

From af29814feb0feab2e92bdf1d2ecbf7e95e5d041f Mon Sep 17 00:00:00 2001
From: Dominik Kleiser <kleiserdominik@outlook.com>
Date: Tue, 13 Apr 2021 05:13:39 +0200
Subject: [PATCH 24/35] Add support for Nortek Doppler Velocity Logs (DVLs) #2
 (#127)

* Add support for Nortek proprietary DVL messages

* Fix tests on systems set to utc timezone
---
 pynmea2/types/proprietary/__init__.py |   1 +
 pynmea2/types/proprietary/nor.py      | 270 ++++++++++++++++++++++++++
 test/test_nor.py                      | 261 +++++++++++++++++++++++++
 3 files changed, 532 insertions(+)
 create mode 100644 pynmea2/types/proprietary/nor.py
 create mode 100644 test/test_nor.py

diff --git a/pynmea2/types/proprietary/__init__.py b/pynmea2/types/proprietary/__init__.py
index 3aae55a..e9be193 100644
--- a/pynmea2/types/proprietary/__init__.py
+++ b/pynmea2/types/proprietary/__init__.py
@@ -8,4 +8,5 @@
 from . import tnl
 from . import ubx
 from . import vtx
+from . import nor
 
diff --git a/pynmea2/types/proprietary/nor.py b/pynmea2/types/proprietary/nor.py
new file mode 100644
index 0000000..c6e75c9
--- /dev/null
+++ b/pynmea2/types/proprietary/nor.py
@@ -0,0 +1,270 @@
+'''
+Support for proprietary messages from Nortek Doppler Velocity Log (DVL).
+'''
+
+from ... import ProprietarySentence, nmea_utils
+from datetime import datetime
+
+class NOR(ProprietarySentence):
+    sentence_types = {}
+
+    def __new__(_cls, manufacturer, data):
+        name = manufacturer + data[0]
+        cls = _cls.sentence_types.get(name, _cls)
+        return super(NOR, cls).__new__(cls)
+
+    def __init__(self, manufacturer, data):
+        self.sentence_type = manufacturer + data[0]
+        super(NOR, self).__init__(manufacturer, data[1:])
+
+    def identifier(self):
+        return 'P%s,' % (self.sentence_type)
+
+##################################################################
+##                                                              ##
+## DVL Bottom Track ASCII formats                               ##
+##                                                              ##
+## Invalid estimates of Velocity are set to set to -32.768.     ##
+## Invalid estimates of Range are set to 0.0.                   ##
+## Invalid estimates of FOM are set to 10.0                     ##
+##################################################################
+
+class NORBT0(NOR, nmea_utils.DatetimeFix):
+    # Bottom Track DF350/DF351 - NMEA $PNORBT1/$PNORBT0
+    # Example: $PNORBT0,1,040721,131335.3341,23.961,-48.122,-32.76800,10.00000,0.00,0x00000000*48
+
+    fields = (
+        ('Beam number', 'beam', int),
+        ('Date', 'datestamp', nmea_utils.datestamp),
+        ('Time', 'timestamp', nmea_utils.timestamp),
+        ('Time (Trigger)', 'dt1', float),
+        ('Time (NMEA)', 'dt2', float),
+        ('Beam Velocity', 'bv', float),
+        ('Figure of Merit', 'fom', float),
+        ('Vertical Distance', 'dist', float),
+        ('Status ', 'stat'),
+    )
+
+class NORBT4(NOR, nmea_utils.DatetimeFix):
+    # Bottom Track DF354/DF355 - NMEA $PNORBT3/$PNORBT4
+    # Example: $PNORBT4,1.234,-1.234,1.234,23.4,12.34567,12.3*09
+
+    fields = (
+        ('Time (Trigger)', 'dt1', float),
+        ('Time (NMEA)', 'dt2', float),
+        ('Speed of Sound', 'sound_speed', float),
+        ('Direction', 'dir', float),
+        ('Figure of Merit', 'fom', float),
+        ('Vertical Distance', 'dist', float),
+    )
+
+class NORBT7(NOR):
+    # Bottom Track DF356/DF357 - NMEA $PNORBT6/$PNORBT7
+    # Example: $PNORBT7,1452244916.7508,1.234,-1.234,0.1234,0.1234,0.1234,12.34,23.45,23.45,23.45,23.45*39
+
+    fields = (
+        ('Ping Time', 'timestamp', lambda x: datetime.utcfromtimestamp(float(x))),
+        ('Time (Trigger)', 'dt1', float),
+        ('Time (NMEA)', 'dt2', float),
+        ('Velocity X', 'vx', float),
+        ('Velocity Y', 'vy', float),
+        ('Velocity Z', 'vz', float),
+        ('Figure of Merit', 'fom', float),
+        ('Vertical Distance Beam 1', 'd1', float),
+        ('Vertical Distance Beam 2', 'd2', float),
+        ('Vertical Distance Beam 3', 'd3', float),
+        ('Vertical Distance Beam 4', 'd4', float),
+    )
+
+class NORBT9(NOR):
+    # Bottom Track DF358/DF359 - NMEA $PNORBT8/$PNORBT9
+    # Example: $PNORBT9,1452244916.7508,1.234,-1.234,0.1234,0.1234,0.1234,12.34,23.45,23.45,23.45,23.45,23.4,1567.8,1.2,12.3,0x000FFFFF*1E
+
+    fields = (
+        ('Ping Time', 'timestamp', lambda x: datetime.utcfromtimestamp(float(x))),
+        ('Time (Trigger)', 'dt1', float),
+        ('Time (NMEA)', 'dt2', float),
+        ('Velocity X', 'vx', float),
+        ('Velocity Y', 'vy', float),
+        ('Velocity Z', 'vz', float),
+        ('Figure of Merit', 'fom', float),
+        ('Vertical Distance Beam 1', 'd1', float),
+        ('Vertical Distance Beam 2', 'd2', float),
+        ('Vertical Distance Beam 3', 'd3', float),
+        ('Vertical Distance Beam 4', 'd4', float),
+        ('Battery Voltage', 'battery_voltage', float),
+        ('Speed of Sound', 'sound_speed', float),
+        ('Pressure', 'pressure', float),
+        ('Temperature', 'temp', float),
+        ('Status ', 'stat'),
+    )
+
+##################################################################
+##                                                              ##
+## DVL Water Track ASCII formats                                ##
+##                                                              ##
+## Invalid estimates of Velocity are set to set to -32.768.     ##
+## Invalid estimates of Range are set to 0.0.                   ##
+## Invalid estimates of FOM are set to 10.0                     ##
+##################################################################
+
+class NORWT4(NOR):
+    # Water Track DF404/DF405 - NMEA $PNORWT3/$PNORWT4
+    # Example: $PNORWT4,1.2345,-1.2345,1.234,23.4,12.34,12.3*1C
+
+    fields = (
+        ('Time Trigger ', 'dt1', float),
+        ('Time NMEA', 'dt2', float),
+        ('Speed of sound', 'sound_speed', float),
+        ('Direction', 'dir', float),
+        ('Figure of Merit', 'fom', float),
+        ('Vertical Distance', 'dist', float),
+    )
+
+class NORWT7(NOR):
+    # Water Track DF406/DF407 - NMEA $PNORWT6/$PNORWT7
+    # Example: $PNORWT7,1452244916.7508,1.234,-1.234,0.1234,0.1234,0.1234,12.34,23.45,23.45,23.45,23.45*2C
+
+    fields = (
+        ('Ping Time', 'timestamp', lambda x: datetime.utcfromtimestamp(float(x))),
+        ('Time (Trigger)', 'dt1', float),
+        ('Time (NMEA)', 'dt2', float),
+        ('Velocity X', 'vx', float),
+        ('Velocity Y', 'vy', float),
+        ('Velocity Z', 'vz', float),
+        ('Figure of Merit', 'fom', float),
+        ('Vertical Distance Beam 1', 'd1', float),
+        ('Vertical Distance Beam 2', 'd2', float),
+        ('Vertical Distance Beam 3', 'd3', float),
+        ('Vertical Distance Beam 4', 'd4', float),
+    )
+
+class NORWT9(NOR):
+    # Water Track DF408/DF409 - NMEA $PNORWT8/$PNORWT9
+    # Example: $PNORWT9,1452244916.7508,1.234,-1.234,0.1234,0.1234,0.1234,12.34,23.45,23.45,23.45,23.45,23.4,1567.8,1.2,12.3,0x000FFFFF*0B
+
+    fields = (
+        ('Ping Time', 'timestamp', lambda x: datetime.utcfromtimestamp(float(x))),
+        ('Time (Trigger)', 'dt1', float),
+        ('Time (NMEA)', 'dt2', float),
+        ('Velocity X', 'vx', float),
+        ('Velocity Y', 'vy', float),
+        ('Velocity Z', 'vz', float),
+        ('Figure of Merit', 'fom', float),
+        ('Vertical Distance Beam 1', 'd1', float),
+        ('Vertical Distance Beam 2', 'd2', float),
+        ('Vertical Distance Beam 3', 'd3', float),
+        ('Vertical Distance Beam 4', 'd4', float),
+        ('Battery Voltage', 'battery_voltage', float),
+        ('Speed of Sound', 'sound_speed', float),
+        ('Pressure', 'pressure', float),
+        ('Temperature', 'temp', float),
+        ('Status ', 'stat'),
+    )
+
+##################################################################
+##                                                              ##
+## DVL Current Profile ASCII formats                            ##
+##                                                              ##
+##################################################################
+
+class NORI1(NOR):
+    # Information Data DF101/DF102 - NMEA Format 1 and 2
+    # Example: $PNORI1,4,123456,3,30,1.00,5.00,BEAM*5B
+
+    fields = (
+        ('Instrument type', 'it', int),
+        ('Head ID', 'sn', int),
+        ('Number of Beams', 'nb', int),
+        ('Number of Cells', 'nc', int),
+        ('Blanking Distance', 'bd', float),
+        ('Cell Size', 'cs', float),
+        ('Coordinate System', 'cy', str),
+    )
+
+class NORS1(NOR, nmea_utils.DatetimeFix):
+    # Sensors Data DF101/DF102 - NMEA Format 1 and 2
+    # Example: $PNORS1,161109,132455,0,34000034,23.9,1500.0,123.4,0.02,45.6,0.02,23.4,0.02,123.456,0.02,24.56*51
+
+    fields = (
+        ('Date', 'datestamp', nmea_utils.datestamp),
+        ('Time', 'timestamp', nmea_utils.timestamp),
+        ('Error Code', 'ec', int),
+        ('Status Code', 'sc'),
+        ('Battery Voltage', 'battery_voltage', float),
+        ('Speed of Sound', 'sound_speed', float),
+        ('Heading', 'heading', float),
+        ('Heading Std. Dev.', 'heading_std', float),
+        ('Pitch', 'pitch', float),
+        ('Pitch Std. Dev.', 'pitch_std', float),
+        ('Roll', 'roll', float),
+        ('Roll Std. Dev.', 'roll_std', float),
+        ('Pressure', 'pressure', float),
+        ('Pressure Std. Dev.', 'pressure_std', float),
+        ('Temperature', 'temp', float),
+    )
+
+
+class NORS4(NOR, nmea_utils.DatetimeFix):
+    # Sensors Data DF103/DF104
+    # Example: $PNORS4,23.6,1530.2,0.0,0.0,0.0,0.000,23.30*66
+
+    fields = (
+        ('Battery Voltage', 'battery_voltage', float),
+        ('Speed of Sound', 'sound_speed', float),
+        ('Heading', 'heading', float),
+        ('Pitch', 'pitch', float),
+        ('Roll', 'roll', float),
+        ('Pressure', 'pressure', float),
+        ('Temperature', 'temp', float),
+    )
+
+
+class NORC1(NOR, nmea_utils.DatetimeFix):
+    # Current Data DF101/DF102 - NMEA Format 1 and 2
+    # Example: $PNORC1,083013,132455,3,11.0,0.332,0.332,0.332,78.9,78.9,78.9,78,78,78*46
+
+    fields = (
+        ('Date', 'datestamp', nmea_utils.datestamp),
+        ('Time', 'timestamp', nmea_utils.timestamp),
+        ('Cell Number', 'cn', int),
+        ('Cell Position', 'cp', float),
+        ('Velocity X', 'vx', float),
+        ('Velocity Y', 'vy', float),
+        ('Velocity Z', 'vz', float),
+        ('Velocity Z2', 'vz2', float),
+        ('Amplitude Beam 1', 'amp1', float),
+        ('Amplitude Beam 2', 'amp2', float),
+        ('Amplitude Beam 3', 'amp3', float),
+        ('Amplitude Beam 4', 'amp4', float),
+        ('Correlation Beam 1', 'r1', int),
+        ('Correlation Beam 2', 'r2', int),
+        ('Correlation Beam 3', 'r3', int),
+        ('Correlation Beam 4', 'r4', int),
+        ('Correlation Beam 4', 'r5', int),
+    )
+
+
+class NORC4(NOR, nmea_utils.DatetimeFix):
+    # Current Data DF103/DF104
+    # Example: $PNORC4,1.5,1.395,227.1,32,32*7A
+
+    fields = (
+        ('Cell Position', 'cp', float),
+        ('Speed', 'sp', float),
+        ('Direction', 'dir', float),
+        ('Correlation', 'r', int),
+        ('Amplitude', 'amp', int),
+    )
+
+
+class NORH4(NOR, nmea_utils.DatetimeFix):
+    # Header Data DF103/DF104
+    # Example: $PNORH4,161109,143459,0,204C0002*38
+
+    fields = (
+        ('Date', 'datestamp', nmea_utils.datestamp),
+        ('Time', 'timestamp', nmea_utils.timestamp),
+        ('Error Code', 'ec', int),
+        ('Status Code', 'sc'),
+    )
diff --git a/test/test_nor.py b/test/test_nor.py
new file mode 100644
index 0000000..a95d7a0
--- /dev/null
+++ b/test/test_nor.py
@@ -0,0 +1,261 @@
+import datetime
+
+import pynmea2
+
+
+def test_norbt0():
+    data = '$PNORBT0,1,040721,131335.3341,23.961,-48.122,-32.76800,10.00000,0.00,0x00000000*48'
+    msg = pynmea2.parse(data)
+    assert type(msg) == pynmea2.nor.NORBT0
+    assert msg.manufacturer == 'NOR'
+    assert msg.sentence_type == 'NORBT0'
+    assert msg.beam == 1
+    assert msg.datestamp == datetime.date(2021, 7, 4)
+    assert msg.timestamp == datetime.time(13, 13, 35, 334100)
+    assert msg.dt1 == 23.961
+    assert msg.dt2 == -48.122
+    assert msg.bv == -32.76800
+    assert msg.fom == 10.00000
+    assert msg.dist == 0.00
+    assert msg.stat == '0x00000000'
+    assert msg.render() == data
+
+
+def test_norbt4():
+    data = '$PNORBT4,1.234,-1.234,1.234,23.4,12.34567,12.3*3D'
+    msg = pynmea2.parse(data)
+    assert type(msg) == pynmea2.nor.NORBT4
+    assert msg.manufacturer == 'NOR'
+    assert msg.sentence_type == 'NORBT4'
+    assert msg.dt1 == 1.234
+    assert msg.dt2 == -1.234
+    assert msg.sound_speed == 1.234
+    assert msg.dir == 23.4
+    assert msg.fom == 12.34567
+    assert msg.dist == 12.3
+    assert msg.render() == data
+
+
+def test_norbt7():
+    data = '$PNORBT7,1452244916.7508,1.234,-1.234,0.1234,0.1234,0.1234,12.34,23.45,23.45,23.45,23.45*39'
+    msg = pynmea2.parse(data)
+    assert type(msg) == pynmea2.nor.NORBT7
+    assert msg.manufacturer == 'NOR'
+    assert msg.sentence_type == 'NORBT7'
+    assert msg.timestamp == datetime.datetime(2016, 1, 8, 9, 21, 56, 750800)
+    assert msg.dt1 == 1.234
+    assert msg.dt2 == -1.234
+    assert msg.vx == 0.1234
+    assert msg.vy == 0.1234
+    assert msg.vz == 0.1234
+    assert msg.fom == 12.34
+    assert msg.d1 == 23.45
+    assert msg.d2 == 23.45
+    assert msg.d3 == 23.45
+    assert msg.d4 == 23.45
+    assert msg.render() == data
+
+
+def test_norbt9():
+    data = '$PNORBT9,1452244916.7508,1.234,-1.234,0.1234,0.1234,0.1234,12.34,23.45,23.45,23.45,23.45,23.4,1567.8,1.2,12.3,0x000FFFFF*1E'
+    msg = pynmea2.parse(data)
+    assert type(msg) == pynmea2.nor.NORBT9
+    assert msg.manufacturer == 'NOR'
+    assert msg.sentence_type == 'NORBT9'
+    assert msg.timestamp == datetime.datetime(2016, 1, 8, 9, 21, 56, 750800)
+    assert msg.dt1 == 1.234
+    assert msg.dt2 == -1.234
+    assert msg.vx == 0.1234
+    assert msg.vy == 0.1234
+    assert msg.vz == 0.1234
+    assert msg.fom == 12.34
+    assert msg.d1 == 23.45
+    assert msg.d2 == 23.45
+    assert msg.d3 == 23.45
+    assert msg.d4 == 23.45
+    assert msg.battery_voltage == 23.4
+    assert msg.sound_speed == 1567.8
+    assert msg.pressure == 1.2
+    assert msg.temp == 12.3
+    assert msg.stat == '0x000FFFFF'
+    assert msg.render() == data
+
+
+def test_norwt4():
+    data = '$PNORWT4,1.2345,-1.2345,1.234,23.4,12.34,12.3*1C'
+    msg = pynmea2.parse(data)
+    assert type(msg) == pynmea2.nor.NORWT4
+    assert msg.manufacturer == 'NOR'
+    assert msg.sentence_type == 'NORWT4'
+    assert msg.dt1 == 1.2345
+    assert msg.dt2 == -1.2345
+    assert msg.sound_speed == 1.234
+    assert msg.dir == 23.4
+    assert msg.fom == 12.34
+    assert msg.dist == 12.3
+    assert msg.render() == data
+
+
+def test_norwt7():
+    data = '$PNORWT7,1452244916.7508,1.234,-1.234,0.1234,0.1234,0.1234,12.34,23.45,23.45,23.45,23.45*2C'
+    msg = pynmea2.parse(data)
+    assert type(msg) == pynmea2.nor.NORWT7
+    assert msg.manufacturer == 'NOR'
+    assert msg.sentence_type == 'NORWT7'
+    assert msg.timestamp == datetime.datetime(2016, 1, 8, 9, 21, 56, 750800)
+    assert msg.dt1 == 1.234
+    assert msg.dt2 == -1.234
+    assert msg.vx == 0.1234
+    assert msg.vy == 0.1234
+    assert msg.vz == 0.1234
+    assert msg.fom == 12.34
+    assert msg.d1 == 23.45
+    assert msg.d2 == 23.45
+    assert msg.d3 == 23.45
+    assert msg.d4 == 23.45
+    assert msg.render() == data
+
+
+def test_norwt9():
+    data = '$PNORWT9,1452244916.7508,1.234,-1.234,0.1234,0.1234,0.1234,12.34,23.45,23.45,23.45,23.45,23.4,1567.8,1.2,12.3,0x000FFFFF*0B'
+    msg = pynmea2.parse(data)
+    assert type(msg) == pynmea2.nor.NORWT9
+    assert msg.manufacturer == 'NOR'
+    assert msg.sentence_type == 'NORWT9'
+    assert msg.timestamp == datetime.datetime(2016, 1, 8, 9, 21, 56, 750800)
+    assert msg.dt1 == 1.234
+    assert msg.dt2 == -1.234
+    assert msg.vx == 0.1234
+    assert msg.vy == 0.1234
+    assert msg.vz == 0.1234
+    assert msg.fom == 12.34
+    assert msg.d1 == 23.45
+    assert msg.d2 == 23.45
+    assert msg.d3 == 23.45
+    assert msg.d4 == 23.45
+    assert msg.battery_voltage == 23.4
+    assert msg.sound_speed == 1567.8
+    assert msg.pressure == 1.2
+    assert msg.temp == 12.3
+    assert msg.stat == '0x000FFFFF'
+    assert msg.render() == data
+
+
+def test_nori1():
+    data = '$PNORI1,4,123456,3,30,1.00,5.00,BEAM*5B'
+    msg = pynmea2.parse(data)
+    assert type(msg) == pynmea2.nor.NORI1
+    assert msg.manufacturer == 'NOR'
+    assert msg.sentence_type == 'NORI1'
+    assert msg.it == 4
+    assert msg.sn == 123456
+    assert msg.nb == 3
+    assert msg.nc == 30
+    assert msg.bd == 1.00
+    assert msg.cs == 5.00
+    assert msg.cy == 'BEAM'
+    assert msg.render() == data
+
+
+def test_nors1():
+    data = '$PNORS1,161109,132455,0,34000034,23.9,1500.0,123.4,0.02,45.6,0.02,23.4,0.02,123.456,0.02,24.56*51'
+    msg = pynmea2.parse(data)
+    assert type(msg) == pynmea2.nor.NORS1
+    assert msg.manufacturer == 'NOR'
+    assert msg.sentence_type == 'NORS1'
+    assert msg.datestamp == datetime.date(2009, 11, 16)
+    assert msg.timestamp == datetime.time(13, 24, 55)
+    assert msg.ec == 0
+    assert msg.sc == '34000034'
+    assert msg.battery_voltage == 23.9
+    assert msg.sound_speed == 1500.0
+    assert msg.heading == 123.4
+    assert msg.heading_std == 0.02
+    assert msg.pitch == 45.6
+    assert msg.pitch_std == 0.02
+    assert msg.roll == 23.4
+    assert msg.roll_std == 0.02
+    assert msg.pressure == 123.456
+    assert msg.pressure_std == 0.02
+    assert msg.temp == 24.56
+    assert msg.render() == data
+
+
+def test_nors4():
+    data = '$PNORS4,23.6,1530.2,0.0,0.0,0.0,0.000,23.30*66'
+    msg = pynmea2.parse(data)
+    assert type(msg) == pynmea2.nor.NORS4
+    assert msg.manufacturer == 'NOR'
+    assert msg.sentence_type == 'NORS4'
+    assert msg.battery_voltage == 23.6
+    assert msg.sound_speed == 1530.2
+    assert msg.heading == 0.0
+    assert msg.pitch == 0.0
+    assert msg.roll == 0.0
+    assert msg.pressure == 0.0
+    assert msg.temp == 23.30
+    assert msg.render() == data
+
+
+def test_norc1():
+    data = '$PNORC1,161109,132455,3,11.0,0.332,0.332,0.332,0.332,78.9,78.9,78.9,78.9,78,78,78,78*56'
+    msg = pynmea2.parse(data)
+    assert type(msg) == pynmea2.nor.NORC1
+    assert msg.manufacturer == 'NOR'
+    assert msg.sentence_type == 'NORC1'
+    assert msg.datetime == datetime.datetime(2009, 11, 16, 13, 24, 55)
+    assert msg.cn == 3
+    assert msg.cp == 11.0
+    assert msg.vx == 0.332
+    assert msg.vy == 0.332
+    assert msg.vz == 0.332
+    assert msg.vz2 == 0.332
+    assert msg.amp1 == 78.9
+    assert msg.amp2 == 78.9
+    assert msg.amp3 == 78.9
+    assert msg.amp4 == 78.9
+    assert msg.r1 == 78
+    assert msg.r2 == 78
+    assert msg.r3 == 78
+    assert msg.r4 == 78
+    assert msg.render() == data
+
+
+def test_norc4():
+    data = '$PNORC4,1.5,1.395,227.1,32,32*7A'
+    msg = pynmea2.parse(data)
+    assert type(msg) == pynmea2.nor.NORC4
+    assert msg.manufacturer == 'NOR'
+    assert msg.sentence_type == 'NORC4'
+    assert msg.cp == 1.5
+    assert msg.sp == 1.395
+    assert msg.dir == 227.1
+    assert msg.r == 32
+    assert msg.amp == 32
+    assert msg.render() == data
+
+
+def test_norh4():
+    data = '$PNORH4,161109,143459,0,204C0002*38'
+    msg = pynmea2.parse(data)
+    assert type(msg) == pynmea2.nor.NORH4
+    assert msg.manufacturer == 'NOR'
+    assert msg.sentence_type == 'NORH4'
+    assert msg.datestamp == datetime.date(2009, 11, 16)
+    assert msg.timestamp == datetime.time(14, 34, 59)
+    assert msg.ec == 0
+    assert msg.sc == '204C0002'
+    assert msg.render() == data
+
+
+def test_nor_undefined():
+    '''
+    Test that non-NOR messages still fall back to the generic NOR type
+    '''
+    data = '$PNORTT3,XYZ,123'
+    msg = pynmea2.parse(data)
+    assert type(msg) == pynmea2.nor.NOR
+    assert msg.manufacturer == 'NOR'
+    assert msg.sentence_type == 'NORTT3'
+    assert msg.data == ['XYZ', '123']
+    assert msg.render(checksum=False) == data

From c546442d1ba38e488f47ef8a190eb5e890260aa2 Mon Sep 17 00:00:00 2001
From: Tom Flanagan <tom@zkpq.ca>
Date: Mon, 12 Apr 2021 20:15:41 -0700
Subject: [PATCH 25/35] Bump version / CI test (#126)

* Update _version.py

* Bump version
---
 pynmea2/_version.py | 2 +-
 test/test_pynmea.py | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/pynmea2/_version.py b/pynmea2/_version.py
index 5fe69a4..733b764 100644
--- a/pynmea2/_version.py
+++ b/pynmea2/_version.py
@@ -1 +1 @@
-__version__ = '1.17.0'
+__version__ = '1.18.0'
diff --git a/test/test_pynmea.py b/test/test_pynmea.py
index 5379e20..b1867c6 100644
--- a/test/test_pynmea.py
+++ b/test/test_pynmea.py
@@ -5,7 +5,7 @@
 
 
 def test_version():
-    version = '1.17.0'
+    version = '1.18.0'
     assert pynmea2.version == version
     assert pynmea2.__version__ == version
 

From 8bad8a676d947c66b5f0539385cd6a2a0a7a9d5b Mon Sep 17 00:00:00 2001
From: Randy Pittman <randallpittman@outlook.com>
Date: Fri, 21 May 2021 13:53:00 -0700
Subject: [PATCH 26/35] Correct PASHR (ASHRATT) Heave field attrname (#129)

* Correct PASHR (ASHRATT) Heave field attrname

In the ASHRATT sentence type the "Heave" field had "heading" for the attr name instead of "heave". This corrects that.
---
 pynmea2/types/proprietary/ash.py | 2 +-
 test/test_ash.py                 | 5 +++--
 2 files changed, 4 insertions(+), 3 deletions(-)

diff --git a/pynmea2/types/proprietary/ash.py b/pynmea2/types/proprietary/ash.py
index 0715bef..aec2bd2 100644
--- a/pynmea2/types/proprietary/ash.py
+++ b/pynmea2/types/proprietary/ash.py
@@ -47,7 +47,7 @@ def __init__(self, *args, **kwargs):
         ('Is True Heading', 'is_true_heading'),
         ('Roll Angle', 'roll', float),
         ('Pitch Angle', 'pitch', float),
-        ('Heave', 'heading', float),
+        ('Heave', 'heave', float),
         ('Roll Accuracy Estimate', 'roll_accuracy', float),
         ('Pitch Accuracy Estimate', 'pitch_accuracy', float),
         ('Heading Accuracy Estimate', 'heading_accuracy', float),
diff --git a/test/test_ash.py b/test/test_ash.py
index 24a2bac..37ad969 100644
--- a/test/test_ash.py
+++ b/test/test_ash.py
@@ -14,16 +14,17 @@ def test_ashrltn():
 
 
 def test_ashratt():
-    data = '$PASHR,130533.620,0.311,T,-80.467,-1.395,,0.066,0.067,0.215,2,3*0B'
+    data = '$PASHR,130533.620,0.311,T,-80.467,-1.395,0.25,0.066,0.067,0.215,2,3*12'
     msg = pynmea2.parse(data)
     assert type(msg) == pynmea2.ash.ASHRATT
-    assert msg.data == ['R', '130533.620', '0.311', 'T', '-80.467', '-1.395', '', '0.066', '0.067', '0.215', '2', '3']
+    assert msg.data == ['R', '130533.620', '0.311', 'T', '-80.467', '-1.395', '0.25', '0.066', '0.067', '0.215', '2', '3']
     assert msg.manufacturer == 'ASH'
     assert msg.timestamp == datetime.time(13, 5, 33, 620000)
     assert msg.true_heading == 0.311
     assert msg.is_true_heading == 'T'
     assert msg.roll == -80.467
     assert msg.pitch == -1.395
+    assert msg.heave == 0.25
     assert msg.roll_accuracy == 0.066
     assert msg.pitch_accuracy == 0.067
     assert msg.heading_accuracy == 0.215

From a0d60b62ab2242af7b3a64c415947eb52a3638a5 Mon Sep 17 00:00:00 2001
From: Tom Flanagan <tom@zkpq.ca>
Date: Mon, 6 Dec 2021 22:19:47 -0800
Subject: [PATCH 27/35] Fix error message for bad data in LatLonFix

---
 pynmea2/nmea_utils.py |  5 ++++-
 test/test_utils.py    | 17 +++++++++++++++++
 2 files changed, 21 insertions(+), 1 deletion(-)
 create mode 100644 test/test_utils.py

diff --git a/pynmea2/nmea_utils.py b/pynmea2/nmea_utils.py
index 9f1bf45..2dc93eb 100644
--- a/pynmea2/nmea_utils.py
+++ b/pynmea2/nmea_utils.py
@@ -39,7 +39,10 @@ def dm_to_sd(dm):
     # '12319.943281'
     if not dm or dm == '0':
         return 0.
-    d, m = re.match(r'^(\d+)(\d\d\.\d+)$', dm).groups()
+    r = re.match(r'^(\d+)(\d\d\.\d+)$', dm)
+    if not r:
+        raise ValueError("Geographic coordinate value '{}' is not valid DDDMM.MMM".format(dm))
+    d, m = r.groups()
     return float(d) + float(m) / 60
 
 
diff --git a/test/test_utils.py b/test/test_utils.py
new file mode 100644
index 0000000..cb2f4fa
--- /dev/null
+++ b/test/test_utils.py
@@ -0,0 +1,17 @@
+import pytest
+import pynmea2
+import pynmea2.nmea_utils
+
+
+def test_GGA():
+    data = "$GPGGA,184353.07,1929.045,S,02410.506,E,1,04,2.6,100.00,M,-33.9,M,,0000*6D"
+    msg = pynmea2.parse(data)
+    assert msg.latitude == -19.484083333333334
+    assert msg.longitude == 24.1751
+    assert msg.is_valid == True
+
+def test_latlon():
+    data = "$GPGGA,161405.680,37.352387,N,121.953086,W,1,10,0.01,-110.342552,M,0.000000,M,,0000,*4E"
+    msg = pynmea2.parse(data)
+    with pytest.raises(ValueError):
+        x =  msg.latitude

From b7898b70b5075269f44593539efb2f28c2904cf5 Mon Sep 17 00:00:00 2001
From: Tom Flanagan <tom@zkpq.ca>
Date: Tue, 26 Jul 2022 23:12:24 -0700
Subject: [PATCH 28/35] Fix travis (#147)

Update .travis.yml
---
 .travis.yml | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/.travis.yml b/.travis.yml
index b7e5540..639892c 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -8,18 +8,20 @@ python:
   - "3.7"
   - "3.8"
   - "3.9"
+  - "3.10-dev"
   - "pypy"
   - "pypy3"
 
 # command to install dependencies, e.g. pip install -r requirements.txt --use-mirrors
 install:
     - pip install pip --upgrade
+    - pip install importlib_metadata --upgrade # fix for broken py3.7
     - pip install pytest --upgrade
     # - pip install pylint --upgrade
 
 # command to run tests, e.g. python setup.py test
 script:
-    - python setup.py sdist --format=zip
+    - python setup.py sdist --formats=zip
     - pip install dist/pynmea2*.zip
     - py.test
     # - pylint -E pynmea2 ## pylint is not backwards compatible with itself

From 988c297ce82d976db9094b435a1aa290e7d5b9ed Mon Sep 17 00:00:00 2001
From: Brent Barbachem <barbacbd@dukes.jmu.edu>
Date: Thu, 28 Jul 2022 01:19:08 -0400
Subject: [PATCH 29/35] Added the additional [optional] types to the RMC
 message including: (#144)

* Added the additional [optional] types to the RMC message including:
- Mode Indicator
- Navigation Status

The tests were added as a single test case with many subcases.

* ** reformat the super call to meet requirements for py2.7

* Update nmea_utils.py

* Update nmea_utils.py

* Update nmea_utils.py

Co-authored-by: Tom Flanagan <tom@zkpq.ca>
---
 pynmea2/nmea_utils.py   | 12 +++++++
 pynmea2/types/talker.py |  6 ++--
 test/test_types.py      | 70 +++++++++++++++++++++++++++++++++++++++++
 3 files changed, 86 insertions(+), 2 deletions(-)

diff --git a/pynmea2/nmea_utils.py b/pynmea2/nmea_utils.py
index 2dc93eb..8cb64e8 100644
--- a/pynmea2/nmea_utils.py
+++ b/pynmea2/nmea_utils.py
@@ -112,6 +112,18 @@ def is_valid(self):
         return self.status == 'A'
 
 
+class ValidRMCStatusFix(ValidStatusFix):
+    #pylint: disable=no-member
+    @property
+    def is_valid(self):
+        status = super(ValidRMCStatusFix, self).is_valid
+        if self.name_to_idx["mode_indicator"] < len(self.data):
+            status &= self.mode_indicator in tuple('ADEFMPRS')
+        if self.name_to_idx["nav_status"] < len(self.data):
+            status &= self.nav_status in tuple('SCU')
+        return status
+
+
 class ValidGSAFix(object):
     #pylint: disable=no-member
     @property
diff --git a/pynmea2/types/talker.py b/pynmea2/types/talker.py
index 547d003..d27ddfe 100644
--- a/pynmea2/types/talker.py
+++ b/pynmea2/types/talker.py
@@ -344,7 +344,7 @@ class RMB(TalkerSentence, ValidStatusFix):
         ("Arrival Alarm", "arrival_alarm"),
     ) # A = Arrived, V = Not arrived
 
-class RMC(TalkerSentence, ValidStatusFix, LatLonFix, DatetimeFix):
+class RMC(TalkerSentence, ValidRMCStatusFix, LatLonFix, DatetimeFix):
     """ Recommended Minimum Specific GPS/TRANSIT Data
     """
     fields = (
@@ -359,6 +359,8 @@ class RMC(TalkerSentence, ValidStatusFix, LatLonFix, DatetimeFix):
         ("Datestamp", "datestamp", datestamp),
         ("Magnetic Variation", "mag_variation"),
         ("Magnetic Variation Direction", "mag_var_dir"),
+        ("Mode Indicator", "mode_indicator"),
+        ("Navigational Status", "nav_status"),
     )
 
 class RTE(TalkerSentence):
@@ -1035,4 +1037,4 @@ class ALK(TalkerSentence,SeaTalk):
         ("Data Byte 7", "data_byte7"),
         ("Data Byte 8", "data_byte8"),
         ("Data Byte 9", "data_byte9")
-    )
\ No newline at end of file
+    )
diff --git a/test/test_types.py b/test/test_types.py
index 759227e..565664d 100644
--- a/test/test_types.py
+++ b/test/test_types.py
@@ -123,6 +123,76 @@ def test_RMC():
     assert msg.render() == data
 
 
+def test_RMC_valid():
+    '''The RMC mode indicator and navigation status values are optional.
+    Test that when supplied the whole message must be valid. When not supplied
+    only test validation against supplied values.
+
+    Supplied means that a `,` exists it does NOT mean that a value had to be
+    supplied in the space provided. See 
+
+    https://orolia.com/manuals/VSP/Content/NC_and_SS/Com/Topics/APPENDIX/NMEA_RMCmess.htm
+
+    for more information about the RMC Message additions.
+    '''
+    msgs = [
+        # Original
+        '$GPRMC,123519.00,A,4807.038,N,01131.000,E,,,230394,,*33',
+        '$GPRMC,123519.00,V,4807.038,N,01131.000,E,,,230394,,*24',
+        '$GPRMC,123519.00,,4807.038,N,01131.000,E,,,230394,,*72',
+        
+        # RMC Timing Messages
+        '$GPRMC,123519.00,A,4807.038,N,01131.000,E,,,230394,,,S*4C',
+        '$GPRMC,123519.00,A,4807.038,N,01131.000,E,,,230394,,,N*51',
+        '$GPRMC,123519.00,A,4807.038,N,01131.000,E,,,230394,,,*1F',
+        '$GPRMC,123519.00,V,4807.038,N,01131.000,E,,,230394,,,S*5B',
+        '$GPRMC,123519.00,V,4807.038,N,01131.000,E,,,230394,,,N*46',
+        '$GPRMC,123519.00,V,4807.038,N,01131.000,E,,,230394,,,*08',
+        '$GPRMC,123519.00,,4807.038,N,01131.000,E,,,230394,,,S*0D',
+        '$GPRMC,123519.00,,4807.038,N,01131.000,E,,,230394,,,N*10',
+        '$GPRMC,123519.00,,4807.038,N,01131.000,E,,,230394,,,*5E',
+        
+        # RMC Nav Messags
+        '$GPRMC,123519.00,A,4807.038,N,01131.000,E,,,230394,,,S,S*33',
+        '$GPRMC,123519.00,A,4807.038,N,01131.000,E,,,230394,,,S,V*36',
+        '$GPRMC,123519.00,A,4807.038,N,01131.000,E,,,230394,,,S,*60',
+        '$GPRMC,123519.00,A,4807.038,N,01131.000,E,,,230394,,,N,A*3C',
+        '$GPRMC,123519.00,A,4807.038,N,01131.000,E,,,230394,,,N,V*2B',
+        '$GPRMC,123519.00,A,4807.038,N,01131.000,E,,,230394,,,N,*7D',
+        '$GPRMC,123519.00,A,4807.038,N,01131.000,E,,,230394,,,,A*72',
+        '$GPRMC,123519.00,A,4807.038,N,01131.000,E,,,230394,,,,V*65',
+        '$GPRMC,123519.00,A,4807.038,N,01131.000,E,,,230394,,,,*33',
+        '$GPRMC,123519.00,V,4807.038,N,01131.000,E,,,230394,,,S,A*36',
+        '$GPRMC,123519.00,V,4807.038,N,01131.000,E,,,230394,,,S,V*21',
+        '$GPRMC,123519.00,V,4807.038,N,01131.000,E,,,230394,,,S,*77',
+        '$GPRMC,123519.00,V,4807.038,N,01131.000,E,,,230394,,,N,A*2B',
+        '$GPRMC,123519.00,V,4807.038,N,01131.000,E,,,230394,,,N,V*3C',
+        '$GPRMC,123519.00,V,4807.038,N,01131.000,E,,,230394,,,N,*6A',
+        '$GPRMC,123519.00,V,4807.038,N,01131.000,E,,,230394,,,,A*65',
+        '$GPRMC,123519.00,V,4807.038,N,01131.000,E,,,230394,,,,V*72',
+        '$GPRMC,123519.00,V,4807.038,N,01131.000,E,,,230394,,,,*24',
+        '$GPRMC,123519.00,,4807.038,N,01131.000,E,,,230394,,,S,A*60',
+        '$GPRMC,123519.00,,4807.038,N,01131.000,E,,,230394,,,S,V*77',
+        '$GPRMC,123519.00,,4807.038,N,01131.000,E,,,230394,,,S,*21',
+        '$GPRMC,123519.00,,4807.038,N,01131.000,E,,,230394,,,N,A*7D',
+        '$GPRMC,123519.00,,4807.038,N,01131.000,E,,,230394,,,N,V*6A',
+        '$GPRMC,123519.00,,4807.038,N,01131.000,E,,,230394,,,N,*3C',
+        '$GPRMC,123519.00,,4807.038,N,01131.000,E,,,230394,,,,A*33',
+        '$GPRMC,123519.00,,4807.038,N,01131.000,E,,,230394,,,,V*24',
+        '$GPRMC,123519.00,,4807.038,N,01131.000,E,,,230394,,,,*72',
+    ]
+
+    # only the first of each section is valid
+    expected = [False] * 39
+    expected[0] = True
+    expected[3] = True
+    expected[12] = True
+
+    for i, msg in enumerate(msgs):
+        parsed = pynmea2.parse(msg)
+        assert expected[i] == parsed.is_valid
+
+
 def test_TXT():
     data = '$GNTXT,01,01,02,ROM BASE 2.01 (75331) Oct 29 2013 13:28:17*44'
     msg = pynmea2.parse(data)

From 43064671a955c5c8a0f950c9bc6b6ac2516d8868 Mon Sep 17 00:00:00 2001
From: Tom Flanagan <tom@zkpq.ca>
Date: Sat, 22 Oct 2022 20:22:57 -0700
Subject: [PATCH 30/35] Use UTC for timestamps. Fixes #100 (#151)

use UTC for timestamps. Fixes #100
---
 examples/nmea2gpx.py     | 103 +++++++++++++++++++++++++++++++++++++++
 pynmea2/nmea_utils.py    |  14 +++++-
 pynmea2/types/talker.py  |   6 +--
 test/test_ash.py         |   2 +-
 test/test_nor.py         |   8 +--
 test/test_proprietary.py |   8 +--
 test/test_types.py       |  20 ++++----
 7 files changed, 139 insertions(+), 22 deletions(-)
 create mode 100644 examples/nmea2gpx.py

diff --git a/examples/nmea2gpx.py b/examples/nmea2gpx.py
new file mode 100644
index 0000000..87154ee
--- /dev/null
+++ b/examples/nmea2gpx.py
@@ -0,0 +1,103 @@
+'''
+Convert a NMEA ascii log file into a GPX file
+'''
+
+import argparse
+import datetime
+import logging
+import pathlib
+import re
+import xml.dom.minidom
+
+log = logging.getLogger(__name__)
+
+try:
+  import pynmea2
+except ImportError:
+  import sys
+  import pathlib
+  p = pathlib.Path(__file__).parent.parent
+  sys.path.append(str(p))
+  log.info(sys.path)
+  import pynmea2
+
+
+def main():
+  parser = argparse.ArgumentParser(formatter_class=argparse.RawDescriptionHelpFormatter)
+  parser.add_argument('nmea_file')
+
+  args = parser.parse_args()
+  nmea_file = pathlib.Path(args.nmea_file)
+
+  if m := re.match(r'^(\d{2})(\d{2})(\d{2})', nmea_file.name):
+    date = datetime.date(year=2000 + int(m.group(1)), month=int(m.group(2)), day=int(m.group(3)))
+    log.debug('date parsed from filename: %r', date)
+  else:
+    date = None
+
+  author = 'https://github.com/Knio/pynmea2'
+  doc = xml.dom.minidom.Document()
+  doc.appendChild(root := doc.createElement('gpx'))
+  root.setAttribute('xmlns', "http://www.topografix.com/GPX/1/1")
+  root.setAttribute('version', "1.1")
+  root.setAttribute('creator', author)
+  root.setAttribute('xmlns', "http://www.topografix.com/GPX/1/1")
+  root.setAttribute('xmlns:xsi', "http://www.w3.org/2001/XMLSchema-instance")
+  root.setAttribute('xsi:schemaLocation', "http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd")
+
+  root.appendChild(meta := doc.createElement('metadata'))
+  root.appendChild(trk := doc.createElement('trk'))
+  meta.appendChild(meta_name := doc.createElement('name'))
+  meta.appendChild(meta_author := doc.createElement('author'))
+  trk.appendChild(trk_name := doc.createElement('name'))
+  trk.appendChild(trkseg := doc.createElement('trkseg'))
+  meta_name.appendChild(doc.createTextNode(nmea_file.name))
+  trk_name. appendChild(doc.createTextNode(nmea_file.name))
+  meta_author.appendChild(author_link := doc.createElement('link'))
+  author_link.setAttribute('href', author)
+  author_link.appendChild(author_text := doc.createElement('text'))
+  author_link.appendChild(author_type := doc.createElement('type'))
+  author_text.appendChild(doc.createTextNode('Pynmea2'))
+  author_type.appendChild(doc.createTextNode('text/html'))
+
+  for line in open(args.nmea_file):
+    try:
+      msg = pynmea2.parse(line)
+    except Exception as e:
+      log.warning('Couldn\'t parse line: %r', e)
+      continue
+
+    if not (hasattr(msg, 'latitude') and hasattr(msg, 'longitude')):
+      continue
+
+    # if not hasattr(msg, 'altitude'):
+    #   continue
+
+    trkseg.appendChild(trkpt := doc.createElement('trkpt'))
+
+    trkpt.setAttribute('lat', f'{msg.latitude:.6f}')
+    trkpt.setAttribute('lon', f'{msg.longitude:.6f}')
+    if hasattr(msg, 'altitude'):
+      trkpt.appendChild(ele := doc.createElement('ele'))
+      ele.appendChild(doc.createTextNode(f'{msg.altitude:.3f}'))
+
+    # TODO try msg.datetime
+
+    if date:
+      trkpt.appendChild(time := doc.createElement('time'))
+      dt = datetime.datetime.combine(date, msg.timestamp)
+      dts = dt.isoformat(timespec='milliseconds').replace('+00:00', 'Z')
+      time.appendChild(doc.createTextNode(dts))
+
+  xml_data = doc.toprettyxml(
+    indent='  ',
+    newl='\n',
+    encoding='utf8',
+  ).decode('utf8')
+  print(xml_data)
+
+
+
+if __name__ == '__main__':
+  logging.basicConfig(level=logging.DEBUG)
+  main()
\ No newline at end of file
diff --git a/pynmea2/nmea_utils.py b/pynmea2/nmea_utils.py
index 8cb64e8..36f0f95 100644
--- a/pynmea2/nmea_utils.py
+++ b/pynmea2/nmea_utils.py
@@ -2,6 +2,17 @@
 import datetime
 import re
 
+
+# python 2.7 backport
+if not hasattr(datetime, 'timezone'):
+    class UTC(datetime.tzinfo):
+        def utcoffset(self, dt):
+            return datetime.timedelta(0)
+    class timezone(object):
+        utc = UTC()
+    datetime.timezone = timezone
+
+
 def valid(s):
     return s == 'A'
 
@@ -18,7 +29,8 @@ def timestamp(s):
         hour=int(s[0:2]),
         minute=int(s[2:4]),
         second=int(s[4:6]),
-        microsecond=ms)
+        microsecond=ms,
+        tzinfo=datetime.timezone.utc)
     return t
 
 
diff --git a/pynmea2/types/talker.py b/pynmea2/types/talker.py
index d27ddfe..8c00c7a 100644
--- a/pynmea2/types/talker.py
+++ b/pynmea2/types/talker.py
@@ -507,7 +507,7 @@ class XTE(TalkerSentence):
     )
 
 
-class ZDA(TalkerSentence):
+class ZDA(TalkerSentence, DatetimeFix):
     fields = (
         ("Timestamp", "timestamp", timestamp), # hhmmss.ss = UTC
         ("Day", "day", int), # 01 to 31
@@ -526,9 +526,9 @@ def tzinfo(self):
         return TZInfo(self.local_zone, self.local_zone_minutes)
 
     @property
-    def datetime(self):
+    def localdatetime(self):
         d = datetime.datetime.combine(self.datestamp, self.timestamp)
-        return d.replace(tzinfo=self.tzinfo)
+        return d.astimezone(self.tzinfo)
 
 
 
diff --git a/test/test_ash.py b/test/test_ash.py
index 37ad969..b7a9425 100644
--- a/test/test_ash.py
+++ b/test/test_ash.py
@@ -19,7 +19,7 @@ def test_ashratt():
     assert type(msg) == pynmea2.ash.ASHRATT
     assert msg.data == ['R', '130533.620', '0.311', 'T', '-80.467', '-1.395', '0.25', '0.066', '0.067', '0.215', '2', '3']
     assert msg.manufacturer == 'ASH'
-    assert msg.timestamp == datetime.time(13, 5, 33, 620000)
+    assert msg.timestamp == datetime.time(13, 5, 33, 620000, tzinfo=datetime.timezone.utc)
     assert msg.true_heading == 0.311
     assert msg.is_true_heading == 'T'
     assert msg.roll == -80.467
diff --git a/test/test_nor.py b/test/test_nor.py
index a95d7a0..2c020b5 100644
--- a/test/test_nor.py
+++ b/test/test_nor.py
@@ -11,7 +11,7 @@ def test_norbt0():
     assert msg.sentence_type == 'NORBT0'
     assert msg.beam == 1
     assert msg.datestamp == datetime.date(2021, 7, 4)
-    assert msg.timestamp == datetime.time(13, 13, 35, 334100)
+    assert msg.timestamp == datetime.time(13, 13, 35, 334100, tzinfo=datetime.timezone.utc)
     assert msg.dt1 == 23.961
     assert msg.dt2 == -48.122
     assert msg.bv == -32.76800
@@ -164,7 +164,7 @@ def test_nors1():
     assert msg.manufacturer == 'NOR'
     assert msg.sentence_type == 'NORS1'
     assert msg.datestamp == datetime.date(2009, 11, 16)
-    assert msg.timestamp == datetime.time(13, 24, 55)
+    assert msg.timestamp == datetime.time(13, 24, 55, tzinfo=datetime.timezone.utc)
     assert msg.ec == 0
     assert msg.sc == '34000034'
     assert msg.battery_voltage == 23.9
@@ -203,7 +203,7 @@ def test_norc1():
     assert type(msg) == pynmea2.nor.NORC1
     assert msg.manufacturer == 'NOR'
     assert msg.sentence_type == 'NORC1'
-    assert msg.datetime == datetime.datetime(2009, 11, 16, 13, 24, 55)
+    assert msg.datetime == datetime.datetime(2009, 11, 16, 13, 24, 55, tzinfo=datetime.timezone.utc)
     assert msg.cn == 3
     assert msg.cp == 11.0
     assert msg.vx == 0.332
@@ -242,7 +242,7 @@ def test_norh4():
     assert msg.manufacturer == 'NOR'
     assert msg.sentence_type == 'NORH4'
     assert msg.datestamp == datetime.date(2009, 11, 16)
-    assert msg.timestamp == datetime.time(14, 34, 59)
+    assert msg.timestamp == datetime.time(14, 34, 59, tzinfo=datetime.timezone.utc)
     assert msg.ec == 0
     assert msg.sc == '204C0002'
     assert msg.render() == data
diff --git a/test/test_proprietary.py b/test/test_proprietary.py
index 3e6a526..58995f8 100644
--- a/test/test_proprietary.py
+++ b/test/test_proprietary.py
@@ -138,7 +138,7 @@ def test_ubx00():
     assert type(msg) == pynmea2.ubx.UBX00
     assert msg.identifier() == 'PUBX'
     assert msg.ubx_type == '00'
-    assert msg.timestamp == datetime.time(7, 44, 40)
+    assert msg.timestamp == datetime.time(7, 44, 40, tzinfo=datetime.timezone.utc)
     assert msg.latitude == 47.06236716666667
     assert msg.lat_dir == 'N'
     assert msg.render() == data
@@ -157,7 +157,7 @@ def test_ubx04():
     msg = pynmea2.parse(data)
     assert type(msg) == pynmea2.ubx.UBX04
     assert msg.date == datetime.date(2014, 10, 13)
-    assert msg.time == datetime.time(7, 38, 24)
+    assert msg.time == datetime.time(7, 38, 24, tzinfo=datetime.timezone.utc)
     assert msg.clk_bias == 495176
     assert msg.render() == data
 
@@ -239,7 +239,7 @@ def test_KWDWPL():
     data = "$PKWDWPL,053125,V,4531.7900,N,12253.4800,W,,,200320,,AC7FD-1,/-*10"
     msg = pynmea2.parse(data)
     assert msg.manufacturer == "KWD"
-    assert msg.timestamp == datetime.time(5, 31, 25)
+    assert msg.timestamp == datetime.time(5, 31, 25, tzinfo=datetime.timezone.utc)
     assert msg.status == 'V'
     assert msg.is_valid == False
     assert msg.lat == '4531.7900'
@@ -249,7 +249,7 @@ def test_KWDWPL():
     assert msg.sog == None
     assert msg.cog == None
     assert msg.datestamp == datetime.date(2020, 3, 20)
-    assert msg.datetime == datetime.datetime(2020, 3, 20, 5, 31, 25)
+    assert msg.datetime == datetime.datetime(2020, 3, 20, 5, 31, 25, tzinfo=datetime.timezone.utc)
     assert msg.altitude == None
     assert msg.wname == 'AC7FD-1'
     assert msg.ts == '/-'
diff --git a/test/test_types.py b/test/test_types.py
index 565664d..1164d38 100644
--- a/test/test_types.py
+++ b/test/test_types.py
@@ -13,7 +13,7 @@ def test_GGA():
     assert isinstance(msg, pynmea2.GGA)
 
     # Timestamp
-    assert msg.timestamp        == datetime.time(18, 43, 53, 70000)
+    assert msg.timestamp        == datetime.time(18, 43, 53, 70000, tzinfo=datetime.timezone.utc)
     # Latitude
     assert msg.lat              == '1929.045'
     # Latitude Direction
@@ -99,7 +99,7 @@ def test_GST():
     data = "$GPGST,172814.0,0.006,0.023,0.020,273.6,0.023,0.020,0.031*6A"
     msg = pynmea2.parse(data)
     assert isinstance(msg, pynmea2.GST)
-    assert msg.timestamp == datetime.time(hour=17, minute=28, second=14)
+    assert msg.timestamp == datetime.time(hour=17, minute=28, second=14, tzinfo=datetime.timezone.utc)
     assert msg.rms == 0.006
     assert msg.std_dev_major == 0.023
     assert msg.std_dev_minor == 0.020
@@ -114,11 +114,11 @@ def test_RMC():
     data = '''$GPRMC,225446,A,4916.45,N,12311.12,W,000.5,054.7,191194,020.3,E*68'''
     msg = pynmea2.parse(data)
     assert isinstance(msg, pynmea2.RMC)
-    assert msg.timestamp == datetime.time(hour=22, minute=54, second=46)
+    assert msg.timestamp == datetime.time(hour=22, minute=54, second=46, tzinfo=datetime.timezone.utc)
     assert msg.datestamp == datetime.date(1994, 11, 19)
     assert msg.latitude == 49.274166666666666
     assert msg.longitude == -123.18533333333333
-    assert msg.datetime == datetime.datetime(1994, 11, 19, 22, 54, 46)
+    assert msg.datetime == datetime.datetime(1994, 11, 19, 22, 54, 46, tzinfo=datetime.timezone.utc)
     assert msg.is_valid == True
     assert msg.render() == data
 
@@ -129,7 +129,7 @@ def test_RMC_valid():
     only test validation against supplied values.
 
     Supplied means that a `,` exists it does NOT mean that a value had to be
-    supplied in the space provided. See 
+    supplied in the space provided. See
 
     https://orolia.com/manuals/VSP/Content/NC_and_SS/Com/Topics/APPENDIX/NMEA_RMCmess.htm
 
@@ -140,7 +140,7 @@ def test_RMC_valid():
         '$GPRMC,123519.00,A,4807.038,N,01131.000,E,,,230394,,*33',
         '$GPRMC,123519.00,V,4807.038,N,01131.000,E,,,230394,,*24',
         '$GPRMC,123519.00,,4807.038,N,01131.000,E,,,230394,,*72',
-        
+
         # RMC Timing Messages
         '$GPRMC,123519.00,A,4807.038,N,01131.000,E,,,230394,,,S*4C',
         '$GPRMC,123519.00,A,4807.038,N,01131.000,E,,,230394,,,N*51',
@@ -151,7 +151,7 @@ def test_RMC_valid():
         '$GPRMC,123519.00,,4807.038,N,01131.000,E,,,230394,,,S*0D',
         '$GPRMC,123519.00,,4807.038,N,01131.000,E,,,230394,,,N*10',
         '$GPRMC,123519.00,,4807.038,N,01131.000,E,,,230394,,,*5E',
-        
+
         # RMC Nav Messags
         '$GPRMC,123519.00,A,4807.038,N,01131.000,E,,,230394,,,S,S*33',
         '$GPRMC,123519.00,A,4807.038,N,01131.000,E,,,230394,,,S,V*36',
@@ -204,14 +204,16 @@ def test_ZDA():
     data = '''$GPZDA,010203.05,06,07,2008,-08,30'''
     msg = pynmea2.parse(data)
     assert isinstance(msg, pynmea2.ZDA)
-    assert msg.timestamp == datetime.time(hour=1, minute=2, second=3, microsecond=50000)
+    assert msg.timestamp == datetime.time(hour=1, minute=2, second=3, microsecond=50000, tzinfo=datetime.timezone.utc)
     assert msg.day == 6
     assert msg.month == 7
     assert msg.year == 2008
+    assert msg.tzinfo.utcoffset(0) == datetime.timedelta(hours=-8, minutes=30)
     assert msg.local_zone == -8
     assert msg.local_zone_minutes == 30
     assert msg.datestamp == datetime.date(2008, 7, 6)
-    assert msg.datetime == datetime.datetime(2008, 7, 6, 1, 2, 3, 50000, msg.tzinfo)
+    assert msg.datetime == datetime.datetime(2008, 7, 6, 1, 2, 3, 50000, tzinfo=datetime.timezone.utc)
+    assert msg.localdatetime == datetime.datetime(2008, 7, 5, 17, 32, 3, 50000, tzinfo=msg.tzinfo)
 
 def test_VPW():
     data = "$XXVPW,1.2,N,3.4,M"

From 5d3d2013bff9c5bce2e14132d21fff865b1e58fd Mon Sep 17 00:00:00 2001
From: Tom Flanagan <tom@zkpq.ca>
Date: Thu, 19 Jan 2023 12:51:50 -0800
Subject: [PATCH 31/35] bump version

---
 pynmea2/_version.py | 2 +-
 test/test_pynmea.py | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/pynmea2/_version.py b/pynmea2/_version.py
index 733b764..a71f144 100644
--- a/pynmea2/_version.py
+++ b/pynmea2/_version.py
@@ -1 +1 @@
-__version__ = '1.18.0'
+__version__ = '1.19.0'
diff --git a/test/test_pynmea.py b/test/test_pynmea.py
index b1867c6..0b12fcb 100644
--- a/test/test_pynmea.py
+++ b/test/test_pynmea.py
@@ -5,7 +5,7 @@
 
 
 def test_version():
-    version = '1.18.0'
+    version = '1.19.0'
     assert pynmea2.version == version
     assert pynmea2.__version__ == version
 

From 5f27ba40b329a448b77d7ab47a10a2c23e9f2e13 Mon Sep 17 00:00:00 2001
From: Tom Flanagan <tom@zkpq.ca>
Date: Mon, 16 Oct 2023 22:32:05 -0700
Subject: [PATCH 32/35] ci workflow

---
 .github/workflows/.coveragerc |  2 ++
 .github/workflows/ci.yml      | 54 +++++++++++++++++++++++++++++++++++
 .travis.yml                   | 33 ---------------------
 README.md                     |  2 +-
 4 files changed, 57 insertions(+), 34 deletions(-)
 create mode 100644 .github/workflows/.coveragerc
 create mode 100644 .github/workflows/ci.yml
 delete mode 100644 .travis.yml

diff --git a/.github/workflows/.coveragerc b/.github/workflows/.coveragerc
new file mode 100644
index 0000000..f396e78
--- /dev/null
+++ b/.github/workflows/.coveragerc
@@ -0,0 +1,2 @@
+[run]
+relative_files = True
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..afb0254
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,54 @@
+# This workflow will install Python dependencies, run tests and lint with a variety of Python versions
+# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python
+
+name: pynmea2
+on:
+  push:
+    branches: ["master"]
+  pull_request:
+    branches: ["master"]
+
+jobs:
+  test:
+    runs-on: ${{ matrix.os }}
+    strategy:
+      fail-fast: false
+      matrix:
+       python: ["3"]
+       os: ["ubuntu-latest"]
+       include:
+          - {python: "3.8", os: "ubuntu-22.04"}
+          - {python: "3.9", os: "ubuntu-22.04"}
+          - {python: "3.10", os: "ubuntu-22.04"}
+          - {python: "3.11", os: "ubuntu-22.04"}
+    steps:
+    - uses: actions/checkout@v3
+    - name: Set up Python ${{ matrix.python }}
+      uses: actions/setup-python@v3
+      with:
+        python-version: ${{ matrix.python }}
+    - name: Install dependencies
+      run: |
+        python -m pip install --upgrade pip
+        python -m pip install pytest
+        python -m pip install flake8
+        python -m pip install importlib_metadata
+    - name: Lint with flake8
+      run: |
+        # stop the build if there are Python syntax errors or undefined names
+        flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
+        flake8 . --count --exit-zero --max-complexity=10 --max-line-length=80 --statistics
+    - name: Build and test
+      run: |
+        python setup.py sdist --formats=zip
+        pip install dist/pynmea2*.zip
+        pytest
+    - name: Coveralls
+      env:
+        COVERAGE_RCFILE: ".github/workflows/.coveragerc"
+        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+      run: |
+        python -m pip install "coverage"
+        python -m pip install "coveralls"
+        coverage run --source=pynmea2 -m pytest
+        python -m coveralls --service=github || true
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 639892c..0000000
--- a/.travis.yml
+++ /dev/null
@@ -1,33 +0,0 @@
-language: python
-
-python:
-  - "2.7"
-  - "3.4"
-  - "3.5"
-  - "3.6"
-  - "3.7"
-  - "3.8"
-  - "3.9"
-  - "3.10-dev"
-  - "pypy"
-  - "pypy3"
-
-# command to install dependencies, e.g. pip install -r requirements.txt --use-mirrors
-install:
-    - pip install pip --upgrade
-    - pip install importlib_metadata --upgrade # fix for broken py3.7
-    - pip install pytest --upgrade
-    # - pip install pylint --upgrade
-
-# command to run tests, e.g. python setup.py test
-script:
-    - python setup.py sdist --formats=zip
-    - pip install dist/pynmea2*.zip
-    - py.test
-    # - pylint -E pynmea2 ## pylint is not backwards compatible with itself
-
-after_success:
-    - pip install coveralls coverage
-    - PYTHONPATH=. coverage run --source=pynmea2 -m pytest
-    - coverage report
-    - coveralls
diff --git a/README.md b/README.md
index 310fcb2..06f922d 100644
--- a/README.md
+++ b/README.md
@@ -12,7 +12,7 @@ The `pynmea2` homepage is located at http://github.com/Knio/pynmea2
 `pynmea2` is compatable with Python 2.7 and Python 3.4+
 
 ![Python version](https://img.shields.io/pypi/pyversions/pynmea2.svg?style=flat)
-[![Build Status](https://www.travis-ci.com/Knio/pynmea2.svg?branch=master)](https://www.travis-ci.com/Knio/pynmea2)
+[![Build status](https://github.com/Knio/pynmea2/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/Knio/pynmea2/actions/workflows/ci.yml?query=branch%3Amaster+)
 [![Coverage status](https://img.shields.io/coveralls/github/Knio/pynmea2/master.svg?style=flat)](https://coveralls.io/r/Knio/pynmea2?branch=master)
 
 ### Installation

From 36cca57dcfea7983f8308c300db73cfd75dd98ef Mon Sep 17 00:00:00 2001
From: nrbray <1032934+nrbray@users.noreply.github.com>
Date: Tue, 17 Oct 2023 06:48:34 +0100
Subject: [PATCH 33/35] =?UTF-8?q?Adds=20support=20for=20$PFLAA=20and=20$PF?=
 =?UTF-8?q?LA=E2=80=A6=20=20=E2=80=A6U=20traffic=20sentences=20from=20the?=
 =?UTF-8?q?=20FLARM=20protocol:=20http://delta-omega.com/download/EDIA/FLA?=
 =?UTF-8?q?RM=5FDataportManual=5Fv3.02E.pdf=20(#149)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

https://github.com/DavisChappins/pynmea2 support for $PFLAA and $PFLAU traffic sentences from the FLARM protocol: http://delta-omega.com/download/EDIA/FLARM_DataportManual_v3.02E.pdf

Co-authored-by: Nigel Bray <nrbray@gmx.net>
---
 pynmea2/types/talker.py | 32 ++++++++++++++++++++++++++++++++
 1 file changed, 32 insertions(+)

diff --git a/pynmea2/types/talker.py b/pynmea2/types/talker.py
index 8c00c7a..c43df37 100644
--- a/pynmea2/types/talker.py
+++ b/pynmea2/types/talker.py
@@ -1038,3 +1038,35 @@ class ALK(TalkerSentence,SeaTalk):
         ("Data Byte 8", "data_byte8"),
         ("Data Byte 9", "data_byte9")
     )
+
+# Implemented by Davis Chappins for FLARM traffic
+#PFLAU: Operating status and priority intruder and obstacle data 
+class LAU(TalkerSentence):
+    fields = (
+        ("RX","RX"),
+        ("TX","TX"),  
+        ("GPS","GPS"),
+        ("Power","Power"),
+        ("AlarmLevel","AlarmLevel"),
+        ("RelativeBearing","RelativeBearing"),
+        ("AlarmType","AlarmType"),
+        ("RelativeVertial","RelativeVertical"),
+        ("RelativeDistance","RelativeDistance"),
+        
+    )
+
+#PFLAA: Data on other moving objects around 
+class LAA(TalkerSentence):
+    fields = (
+        ("AlarmLevel","AlarmLevel"),
+        ("RelativeNorth","RelativeNorth"),
+        ("RelativeEast","RelativeEast"),
+        ("RelativeVertical","RelativeVertical"),
+        ("ID-Type","ID-Type"),
+        ("ID","ID"),
+        ("Track","Track"),
+        ("TurnRate","TurnRate"),
+        ("GroundSpeed","GroundSpeed"),
+        ("ClimbRate","ClimbRate"),
+        ("Type","Type"),
+    )

From b546a71a8b0f9766dc47293c1e2468ef8b47b640 Mon Sep 17 00:00:00 2001
From: nalia3486 <nalia3486@gmail.com>
Date: Tue, 17 Oct 2023 07:49:08 +0200
Subject: [PATCH 34/35] Add hbt and alr (#161)

* Add ALR and HBT sentences

* Add ALR and HBT sentences
---
 pynmea2/types/talker.py | 28 ++++++++++++++++++++++++++++
 test/test_types.py      | 25 +++++++++++++++++++++++++
 2 files changed, 53 insertions(+)

diff --git a/pynmea2/types/talker.py b/pynmea2/types/talker.py
index c43df37..6b56687 100644
--- a/pynmea2/types/talker.py
+++ b/pynmea2/types/talker.py
@@ -43,6 +43,19 @@ class ALM(TalkerSentence):
     )
 
 
+class ALR(TalkerSentence):
+    """ Set alarm state
+        $--ALR,hhmmss.ss,xxx,A,A,c--c*hh<CR><LF>
+    """
+    fields = (
+        ('Time of alarm condition change, UTC', 'timestamp', timestamp),
+        ('Unique alarm number (identifier) at alarm source', 'alarm_num'),
+        ('Alarm condition (A=threshold exceeded, V=not exceeded)', 'alarm_con'),
+        ('Alarm\'s acknowledge state (A=acknowledged, V=unacknowledged)', 'alarm_state'),
+        ('Alarm\'s description text', 'description'),
+    )
+
+
 class APA(TalkerSentence):
     """ Autopilot Sentence "A"
     """
@@ -283,6 +296,21 @@ class GSV(TalkerSentence):
     )  # 00-99 dB
 
 
+class HBT(TalkerSentence):
+    """ Heartbeat supervision sentence
+        Format: $--HBT,<1>,<2>,<3>*hh<CR><LF>
+        e.g. $AIHBT,30,A,5*0D
+    <1> Configured repeat interval
+    <2> Equipment status
+    <3> Sequential sentence identifier
+    """
+    fields = (
+        ("Configured repeat interval", "interval", float),
+        ("Equipment status", "eq_status"),
+        ("Sequential sentence identifier", "seq_sent_iden", int),
+    )
+
+
 class HDG(TalkerSentence):
     """ NMEA 0183 standard Heading, Deviation and Variation
         Format: $HCHDG,<1>,<2>,<3>,<4>,<5>*hh<CR><LF>
diff --git a/test/test_types.py b/test/test_types.py
index 1164d38..22f4123 100644
--- a/test/test_types.py
+++ b/test/test_types.py
@@ -306,3 +306,28 @@ def test_GRS():
     assert msg.sv_res_05 == -0.1
     assert msg.sv_res_06 == 0.5
     assert msg.sv_res_07 == None
+
+
+def test_HBT():
+    data = "$AIHBT,30,A,1*09"
+    msg = pynmea2.parse(data)
+    assert msg.render() == data
+    assert isinstance(msg, pynmea2.HBT)
+    assert msg.talker == 'AI'
+    assert msg.sentence_type == 'HBT'
+    assert msg.interval == 30
+    assert msg.eq_status == 'A'
+    assert msg.seq_sent_iden == 1
+
+
+def test_ALR():
+    data = "$AIALR,,006,V,V,AIS:general failure*1A"
+    msg = pynmea2.parse(data)
+    assert msg.render() == data
+    assert isinstance(msg, pynmea2.ALR)
+    assert msg.talker == 'AI'
+    assert msg.sentence_type == 'ALR'
+    assert msg.alarm_num == '006'
+    assert msg.alarm_con == 'V'
+    assert msg.alarm_state == 'V'
+    assert msg.description == 'AIS:general failure'
\ No newline at end of file

From f88b6cba54d5e7c9695eefc68fb506dd1fc08409 Mon Sep 17 00:00:00 2001
From: DarcyB <darcy@dbitech.ca>
Date: Mon, 16 Oct 2023 22:50:22 -0700
Subject: [PATCH 35/35] Kenwood lmr (#154)

* Initial supoport for proprietary Kenwood Land Mobile Radio AVL Data

* Kenwood FleetSync II
---
 pynmea2/types/proprietary/kwd.py | 132 +++++++++++++++++++++++++++++++
 test/test_proprietary.py         |  71 +++++++++++++++++
 2 files changed, 203 insertions(+)

diff --git a/pynmea2/types/proprietary/kwd.py b/pynmea2/types/proprietary/kwd.py
index ca80945..132572f 100644
--- a/pynmea2/types/proprietary/kwd.py
+++ b/pynmea2/types/proprietary/kwd.py
@@ -6,6 +6,54 @@
 from ... import nmea
 from ... import nmea_utils
 
+class KLD(nmea.ProprietarySentence):
+    sentence_types = {}
+
+    def __new__(_cls, manufacturer, data):
+        name = manufacturer + data[0]
+        cls = _cls.sentence_types.get(name, _cls)
+        return super(KLD, cls).__new__(cls)
+
+    def __init__(self, manufacturer, data):
+        self.sentence_type = manufacturer + data[0]
+        super(KLD, self).__init__(manufacturer, data)
+
+class KND(nmea.ProprietarySentence):
+    sentence_types = {}
+
+    def __new__(_cls, manufacturer, data):
+        name = manufacturer + data[0]
+        cls = _cls.sentence_types.get(name, _cls)
+        return super(KND, cls).__new__(cls)
+
+    def __init__(self, manufacturer, data):
+        self.sentence_type = manufacturer + data[0]
+        super(KND, self).__init__(manufacturer, data)
+
+class KLS(nmea.ProprietarySentence):
+    sentence_types = {}
+
+    def __new__(_cls, manufacturer, data):
+        name = manufacturer + data[0]
+        cls = _cls.sentence_types.get(name, _cls)
+        return super(KLS, cls).__new__(cls)
+
+    def __init__(self, manufacturer, data):
+        self.sentence_type = manufacturer + data[0]
+        super(KLS, self).__init__(manufacturer, data)
+
+class KNS(nmea.ProprietarySentence):
+    sentence_types = {}
+
+    def __new__(_cls, manufacturer, data):
+        name = manufacturer + data[0]
+        cls = _cls.sentence_types.get(name, _cls)
+        return super(KNS, cls).__new__(cls)
+
+    def __init__(self, manufacturer, data):
+        self.sentence_type = manufacturer + data[0]
+        super(KNS, self).__init__(manufacturer, data)
+
 
 class KWD(nmea.ProprietarySentence):
     sentence_types = {}
@@ -102,3 +150,87 @@ class KWDWPL(KWD, nmea_utils.LatLonFix, nmea_utils.DatetimeFix, nmea_utils.Valid
         ("Waypoint Name", "wname"),
         ("Table and Symbol", "ts"),
     )
+
+class KLDS(KLD, nmea_utils.LatLonFix, nmea_utils.DatetimeFix, nmea_utils.ValidStatusFix):
+    """
+    $PKLDS,hhmmss,v,ddmm.mm,ns,dddmm.mm,ew,speed,course,ddmmyy,DD.dd,ewSV,fleet,svid,status,fut*99
+    $PKLDS,001235,A,3544.6650,N,13940.1900,E,015.0,038.8,110498,10.80,W00,100,2000,15,00,*??
+    """
+    fields = (
+        ("Subtype", "subtype"),
+        ("Time of Receipt", "timestamp", nmea_utils.timestamp),
+        ("GPS Status (Void)","status"),
+        ("Latitude", "lat"),
+        ("Latitude Direction", "lat_dir"),
+        ("Longitude", "lon"),
+        ("Longitude Direction", "lon_dir"),
+        ("Speed over Ground Knot", "sog", float),
+        ("Course over Ground", "cog", float),
+        ("Date", "datestamp", nmea_utils.datestamp),
+        ("Magnetic variation", "declination", float),
+        ("Declination Direction", "dec_dir"),
+        ("Fleet", "fleet", Decimal),
+        ("Sender ID", "senderid"),
+        ("Sender Status", "senderstatus", Decimal),
+        ("Future Reserved", "future", Decimal),
+    )
+
+
+
+class KNDS(KND, nmea_utils.LatLonFix, nmea_utils.DatetimeFix, nmea_utils.ValidStatusFix):
+    """
+    $PKNDS,hhmmss,v,ddmm.mm,ns,dddmm.mm,ew,speed,course,ddmmyy,DD.dd,ewSV,svid,status,fut*99
+    $PKNDS,124640,A,4954.1458,N,11923.5992,W,000.0,000.0,120223,19.20,W00,U00002,207,00,*29
+
+    """
+    fields = (
+        ("Subtype", "subtype"),
+        ("Time of Receipt", "timestamp", nmea_utils.timestamp),
+        ("GPS Status (Void)","status"),
+        ("Latitude", "lat"),
+        ("Latitude Direction", "lat_dir"),
+        ("Longitude", "lon"),
+        ("Longitude Direction", "lon_dir"),
+        ("Speed over Ground Knot", "sog", float),
+        ("Course over Ground", "cog", float),
+        ("Date", "datestamp", nmea_utils.datestamp),
+        ("Magnetic variation", "declination", float),
+        ("Declination Direction", "dec_dir"),
+        ("Sender ID", "senderid"),
+        ("Sender Status", "senderstatus", Decimal),
+        ("Future Reserved", "future", Decimal),
+    )
+
+class KLSH(KLS, nmea_utils.LatLonFix, nmea_utils.DatetimeFix, nmea_utils.ValidStatusFix):
+    """
+    $PKLSH,ddmm.mm,ns,dddmm.mm,ew,hhmmss,v,fleet,svid,*99
+    $PKLSH,4000.0000,N,13500.0000,E,021720,A,100,2000,* ??
+    """
+    fields = (
+        ("Subtype", "subtype"),
+        ("Latitude", "lat"),
+        ("Latitude Direction", "lat_dir"),
+        ("Longitude", "lon"),
+        ("Longitude Direction", "lon_dir"),
+        ("Time of Receipt", "timestamp", nmea_utils.timestamp),
+        ("GPS Status (Void)","status"),
+        ("Fleet", "fleet", Decimal),
+        ("Sender ID", "senderid"),
+    )
+
+class KNSH(KNS, nmea_utils.LatLonFix, nmea_utils.DatetimeFix, nmea_utils.ValidStatusFix):
+    """
+    $PKLSH,ddmm.mm,ns,dddmm.mm,ew,hhmmss,v,svid,*99
+    $PKNSH,4000.0000,N,13500.0000,E,021720,A,U00001,* ??
+    """
+    fields = (
+        ("Subtype", "subtype"),
+        ("Latitude", "lat"),
+        ("Latitude Direction", "lat_dir"),
+        ("Longitude", "lon"),
+        ("Longitude Direction", "lon_dir"),
+        ("Time of Receipt", "timestamp", nmea_utils.timestamp),
+        ("GPS Status (Void)","status"),
+        ("Sender ID", "senderid"),
+    )
+
diff --git a/test/test_proprietary.py b/test/test_proprietary.py
index 58995f8..ce6aea3 100644
--- a/test/test_proprietary.py
+++ b/test/test_proprietary.py
@@ -255,3 +255,74 @@ def test_KWDWPL():
     assert msg.ts == '/-'
     assert msg.latitude == 45.529833333333336
     assert msg.longitude == -122.89133333333334
+
+def test_PKNDS():
+    # A sample proprietary Kenwood sentence used for GPS data communications in NEXEDGE Digital
+    data = "$PKNDS,114400,A,4954.1450,N,11923.6043,W,001.4,356.8,130223,19.20,W00,U00002,207,00,*2E"
+    msg = pynmea2.parse(data)
+    assert msg.manufacturer == "KND"
+    assert msg.timestamp == datetime.time(11, 44, 00, tzinfo=datetime.timezone.utc)
+    assert msg.status == 'A'
+    assert msg.is_valid == True
+    assert msg.lat == '4954.1450'
+    assert msg.lat_dir == 'N'
+    assert msg.lon == '11923.6043'
+    assert msg.lon_dir == 'W'
+    assert msg.datestamp == datetime.date(2023, 2, 13)
+    assert msg.datetime == datetime.datetime(2023, 2, 13, 11, 44, 00, tzinfo=datetime.timezone.utc)
+    assert msg.senderid == 'U00002'
+    assert msg.senderstatus  == 207
+    assert msg.latitude == 49.90241666666667
+    assert msg.longitude == -119.393405
+
+def test_PKNSH():
+    # A sample proprietary Kenwood sentence used for GPS data communications in NEXEDGE Digital
+    data = "$PKNSH,4954.1450,N,11923.6043,W,114400,A,U00002,*44"
+    msg = pynmea2.parse(data)
+    assert msg.manufacturer == "KNS"
+    assert msg.timestamp == datetime.time(11, 44, 00, tzinfo=datetime.timezone.utc)
+    assert msg.status == 'A'
+    assert msg.is_valid == True
+    assert msg.lat == '4954.1450'
+    assert msg.lat_dir == 'N'
+    assert msg.lon == '11923.6043'
+    assert msg.lon_dir == 'W'
+    assert msg.senderid == 'U00002'
+    assert msg.latitude == 49.90241666666667
+    assert msg.longitude == -119.393405
+
+def test_PKLDS():
+    # A sample proprietary Kenwood sentence used for GPS data communications in FleetSync II signaling
+    data = "$PKLDS,122434,A,4954.1474,N,11923.6044,W,001.1,194.9,130223,19.20,W00,100,1001,80,00,*60"
+    msg = pynmea2.parse(data)
+    assert msg.manufacturer == "KLD"
+    assert msg.timestamp == datetime.time(12, 24, 34, tzinfo=datetime.timezone.utc)
+    assert msg.status == 'A'
+    assert msg.is_valid == True
+    assert msg.lat == '4954.1474'
+    assert msg.lat_dir == 'N'
+    assert msg.lon == '11923.6044'
+    assert msg.lon_dir == 'W'
+    assert msg.datestamp == datetime.date(2023, 2, 13)
+    assert msg.datetime == datetime.datetime(2023, 2, 13, 12, 24, 34, tzinfo=datetime.timezone.utc)
+    assert msg.senderid == '1001'
+    assert msg.fleet == 100
+    assert msg.latitude == 49.902456666666666
+    assert msg.longitude == -119.39340666666666
+
+def test_PKLSH():
+    # A sample proprietary Kenwood sentence used for GPS data communications in FleetSync II signaling
+    data = "$PKLSH,4954.1474,N,11923.6044,W,122434,A,100,1001,*3F"
+    msg = pynmea2.parse(data)
+    assert msg.manufacturer == "KLS"
+    assert msg.timestamp == datetime.time(12, 24, 34, tzinfo=datetime.timezone.utc)
+    assert msg.status == 'A'
+    assert msg.is_valid == True
+    assert msg.lat == '4954.1474'
+    assert msg.lat_dir == 'N'
+    assert msg.lon == '11923.6044'
+    assert msg.lon_dir == 'W'
+    assert msg.senderid == '1001'
+    assert msg.fleet == 100
+    assert msg.latitude == 49.902456666666666
+    assert msg.longitude == -119.39340666666666