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 - -[](https://travis-ci.org/Knio/pynmea2) -[](https://coveralls.io/r/Knio/pynmea2?branch=master) + +[](https://travis-ci.org/Knio/pynmea2) +[](https://coveralls.io/r/Knio/pynmea2?branch=master) [](https://landscape.io/github/Knio/pynmea2/master) ### Installation @@ -23,7 +19,8 @@ The recommended way to install `pynmea2` is with pip install pynmea2 -[](http://badge.fury.io/py/pynmea2) +[](https://pypi.org/project/pynmea2/) +[](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+ +  [](https://travis-ci.org/Knio/pynmea2) [](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  [](https://travis-ci.org/Knio/pynmea2) [](https://coveralls.io/r/Knio/pynmea2?branch=master) -[](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+ +  [](https://travis-ci.org/Knio/pynmea2) [](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  [](https://travis-ci.org/Knio/pynmea2) [](https://coveralls.io/r/Knio/pynmea2?branch=master) -[](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+  -[](https://travis-ci.org/Knio/pynmea2) +[](https://www.travis-ci.com/Knio/pynmea2) [](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+  -[](https://www.travis-ci.com/Knio/pynmea2) +[](https://github.com/Knio/pynmea2/actions/workflows/ci.yml?query=branch%3Amaster+) [](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