diff --git a/.github/workflows/checkpr.yml b/.github/workflows/checkpr.yml index cb7cbae..ada23b6 100644 --- a/.github/workflows/checkpr.yml +++ b/.github/workflows/checkpr.yml @@ -11,9 +11,9 @@ jobs: python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} - name: Install deploy dependencies diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 441b0b0..b3bf21e 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -11,10 +11,9 @@ jobs: release-build: runs-on: ubuntu-latest steps: - - - uses: actions/checkout@v4 - - name: Set up Python - uses: actions/setup-python@v5 + - uses: actions/checkout@v6 + - name: Set up Python + uses: actions/setup-python@v6 with: python-version: 3.13 @@ -24,7 +23,7 @@ jobs: python -m build - name: Upload distributions # upload build artifacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: release-dists path: dist/ @@ -35,13 +34,12 @@ jobs: name: Upload release to PyPI runs-on: ubuntu-latest environment: - name: pypi + name: pypi permissions: id-token: write steps: - - name: Download all the dists # download build artifacts saved in previous job - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: name: release-dists path: dist/ diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 59b4ea6..8b80734 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -13,9 +13,9 @@ jobs: python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} - name: Install deploy dependencies diff --git a/.vscode/settings.json b/.vscode/settings.json index fc577e7..98ae6e1 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -17,11 +17,10 @@ "C_Cpp.copilotHover": "disabled", "chat.agent.enabled": false, "chat.commandCenter.enabled": false, - "chat.notifyWindowOnConfirmation": false, + "chat.notifyWindowOnConfirmation": "off", "telemetry.feedback.enabled": false, "python.analysis.addHoverSummaries": false, "python-envs.defaultEnvManager": "ms-python.python:venv", - "python-envs.pythonProjects": [], "python.defaultInterpreterPath": "${userHome}/pygpsclient/bin/python3", "python.testing.unittestEnabled": true, "python.testing.unittestArgs": [ diff --git a/README.md b/README.md index e79042d..a2f1f3a 100644 --- a/README.md +++ b/README.md @@ -173,40 +173,43 @@ The `parse()` method accepts the following optional keyword arguments: * `parsebitfield`: 1 = parse bitfields ('X' type properties) as individual bit flags, where defined (default), 0 = leave bitfields as byte sequences * `msgmode`: `GET` (0) (default), `SET` (1), `POLL` (2) -Example A - parsing VERSION output message: +Example A - parsing BESTNAV output message: ```python from pyunigps import GET, VALCKSUM, UNIReader msg = UNIReader.parse( - b'\xaaD\xb5\x00\x11\x004\x01\x00\x00f\t\x8f\xf4\x0e\x02\x00\x00\x00\x00\x00\x00\x00\x00M982R4.10Build5251 HRPT00-S10C-P - ffff48ffff0fffff 2021/11/26 #\x87\x83\xb9' - , + b"\xaaD\xb5YF\x08x\x00\x00\xa0e\t\xe8\...\x98\x15\xa5\xf1", validate=VALCKSUM, parsebitfield=1, ) print(msg) ``` ``` - +"" ``` -The `UNIMessage` object exposes different public attributes depending on its message type or 'identity'. Attributes which are enumerations may have corresponding decodes in `pyunigps.unitypes_decodes` e.g. the `VERSION` message has the following attributes: +The `UNIMessage` object exposes different public attributes depending on its message type or 'identity'. Helper methods are available to convert position coordinates into different formats. Attributes which are enumerations may have corresponding decodes in `pyunigps.unitypes_decodes` e.g. the `BESTNAV` message has the following attributes: ```python -from pyunigps import DEVICE -print(msg) +from pyunigps import POSTYPE, PSRIONOCORR, SOLSTATUS, latlon2dms, llh2iso6709, wnotow2utc print(msg.identity) -print(msg.device) -print(DEVICE[msg.device]) -print(swversion) -print(comptime) +print(wnotow2utc(msg.wno, msg.tow, msg.leapsecond)) +print(msg.lat, msg.lon, msg.hmsl) +print(latlon2dms(msg.lat, msg.lon)) +print(llh2iso6709(msg.lat, msg.lon, msg.hmsl)) +print(msg.solstatus, SOLSTATUS[msg.solstatus]) +print(msg.postype, POSTYPE[msg.postype]) +print(msg.psrionocorr, PSRIONOCORR[msg.psrionocorr]) ``` ``` - -VERSION -18 -UM980 -R4.10Build5251 -2021/11/26 +BESTNAV +2026-02-08 20:58:23+00:00 +43.450634678833644, -1.1303087675041795, 36.32093767914921 +('43°27′2.28484″N', '1°7′49.11156″W') ++43.450634678833644-1.1303087675041794+36.32093767914921CRSWGS_84/ +0 SOL_COMPUTED +16 SINGLE +1 Klobuchareph ``` The `payload` attribute always contains the raw payload as bytes. Attributes within repeating groups are parsed with a two-digit suffix (prn_01, prn_02, etc.). diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index b2e917e..a298b29 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,5 +1,11 @@ # pyunigps Release Notes +### RELEASE 1.0.0 + +1. Update status to Stable. +1. Remove duplicate keys from DEVICE decode. +1. Inherit helper methods from pynmeagps. + ### RELEASE 0.2.0 1. Update for Protocol Specification R1.13 Dec 2025. diff --git a/pyproject.toml b/pyproject.toml index 80aed16..a67fa36 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,7 +14,7 @@ readme = "README.md" requires-python = ">=3.10" classifiers = [ "Operating System :: OS Independent", - "Development Status :: 4 - Beta", + "Development Status :: 5 - Production/Stable", "Environment :: MacOS X", "Environment :: Win32 (MS Windows)", "Environment :: X11 Applications", @@ -32,7 +32,7 @@ classifiers = [ "Topic :: Scientific/Engineering :: GIS", ] -dependencies = ["pynmeagps >= 1.1.0", "pyrtcm>=1.1.10"] +dependencies = ["pynmeagps >= 1.1.2", "pyrtcm>=1.1.11"] [project.urls] homepage = "https://github.com/semuconsulting/pyunigps" diff --git a/src/pyunigps/__init__.py b/src/pyunigps/__init__.py index e8df5c5..20a4579 100644 --- a/src/pyunigps/__init__.py +++ b/src/pyunigps/__init__.py @@ -6,7 +6,24 @@ :license: BSD 3-Clause """ -from pynmeagps import SocketWrapper +from pynmeagps import ( + SocketWrapper, + area, + bearing, + deg2dmm, + deg2dms, + dms2deg, + ecef2llh, + haversine, + latlon2dmm, + latlon2dms, + leapsecond, + llh2ecef, + llh2iso6709, + planar, + utc2wnotow, + wnotow2utc, +) from pyunigps._version import __version__ from pyunigps.exceptions import ( diff --git a/src/pyunigps/_version.py b/src/pyunigps/_version.py index a2eca56..574da53 100644 --- a/src/pyunigps/_version.py +++ b/src/pyunigps/_version.py @@ -8,4 +8,4 @@ :license: BSD 3-Clause """ -__version__ = "0.2.0" +__version__ = "1.0.0" diff --git a/src/pyunigps/unihelpers.py b/src/pyunigps/unihelpers.py index 1b1c65b..5a9b308 100644 --- a/src/pyunigps/unihelpers.py +++ b/src/pyunigps/unihelpers.py @@ -2,6 +2,8 @@ Collection of UNI helper methods which can be used outside the UNIMessage or UNIReader classes. +pyunigps also inherits pynmeagps helper methods. + Created on 6 Oct 2025 :author: semuadmin (Steve Smith) @@ -14,7 +16,7 @@ from types import NoneType from typing import Any -from pynmeagps import leapsecond +from pynmeagps import utc2wnotow import pyunigps.exceptions as une from pyunigps.unitypes_core import ATTTYPE, SCALROUND, U4, UNI_MSGIDS @@ -492,27 +494,6 @@ def nomval(adef: str) -> Any: return val -def utc2wnotow(utc: datetime | NoneType = None) -> tuple[int, int, int]: - """ - Get GPS Week number (wno), Time of Week (tow) in milliseconds - and leapsecond offset for given utc datetime. - - GPS Epoch 0 = 6th Jan 1980 - - :param datetime | NoneType utc: utc datetime (defaults to now if None) - :return: wno, tow, leapsecond - :rtype: tuple[int,int, int] - """ - - if utc is None: - utc = datetime.now(tz=timezone.utc) - ls = leapsecond(utc.replace(tzinfo=None)) # method is not tz aware - ts = ((utc - GPSEPOCH0).total_seconds() + ls) * 1000 - wno = int((utc - GPSEPOCH0).days / 7) - tow = int(ts - wno * 604800000) - return wno, tow, ls - - def val2bytes(val: Any, adef: str) -> bytes: """ Convert value to bytes for given UNI attribute type. diff --git a/src/pyunigps/unimessage.py b/src/pyunigps/unimessage.py index 9f5efa4..f8d90b8 100644 --- a/src/pyunigps/unimessage.py +++ b/src/pyunigps/unimessage.py @@ -14,6 +14,8 @@ from types import NoneType +from pynmeagps import utc2wnotow + from pyunigps.exceptions import UNIMessageError from pyunigps.unihelpers import ( attsiz, @@ -23,7 +25,6 @@ header2bytes, msgname2id, nomval, - utc2wnotow, val2bytes, ) from pyunigps.unitypes_core import ( diff --git a/src/pyunigps/unitypes_decodes.py b/src/pyunigps/unitypes_decodes.py index 54b1c53..e7c7de9 100644 --- a/src/pyunigps/unitypes_decodes.py +++ b/src/pyunigps/unitypes_decodes.py @@ -35,8 +35,6 @@ 24: "UM960L", 26: "UM981", 31: "UM981S", - 24: "UM960L", - 26: "UM981", 40: "UMD982", 41: "UMD981", 42: "UMD981S", diff --git a/tests/pygpsdata_nmea.log b/tests/pygpsdata_nmea.log index c514456..ea402bf 100644 --- a/tests/pygpsdata_nmea.log +++ b/tests/pygpsdata_nmea.log @@ -11,3 +11,6 @@ $GNGSA,A,3,,,,,,,,,,,,,9.62,5.88,7.62,3*08 $GNGSA,A,3,,,,,,,,,,,,,9.62,5.88,7.62,4*0F $GPGSV,3,1,11,01,06,014,08,12,43,207,28,14,06,049,,15,44,171,23,1*6B $GPGSV,3,2,11,17,32,064,16,19,33,094,,20,20,251,31,21,04,354,,1*63 +$GNRMC,075233.00,A,5451.99268133,N,00125.70810842,W,0.009,13.6,240226,0.2,W,A,C*75 +$GNGGA,075233.00,5451.99268133,N,00125.70810842,W,1,28,0.5,103.8322,M,49.2859,M,,*63 +$GNGLL,5451.99268133,N,-0125.70810842,W,075233.00,A,A*76 diff --git a/tests/test_parse.py b/tests/test_parse.py index 84b17a7..53b82a4 100644 --- a/tests/test_parse.py +++ b/tests/test_parse.py @@ -89,6 +89,8 @@ def testparseum981(self): unr = UNIReader(stream) i = 0 for raw, parsed in unr: + if parsed.identity == "BESTNAV": + print(raw) if parsed.identity == "GALEPH": # sanity check, should be ≈ √29,599,800 m self.assertTrue(5440 < parsed.roota < 5441) @@ -354,9 +356,9 @@ def testimmutable(self): ): msg.device = 18 - def testrtcm(self): # test RTCM parsing + def testmixed(self): # test mixed NMEA RTCM parsing EXPECTED_RESULTS = ( - "", + "", "", "", "", @@ -364,7 +366,7 @@ def testrtcm(self): # test RTCM parsing "", "", "", - "", + "", ) i = 0 @@ -375,7 +377,42 @@ def testrtcm(self): # test RTCM parsing parsing=True, parsebitfield=1, validate=VALCKSUM, - msgmode=POLL, + quitonerror=ERR_RAISE, + ) + for raw, parsed in ubr: + # print(f'"{parsed}",') + self.assertEqual(str(parsed), EXPECTED_RESULTS[i]) + i += 1 + self.assertEqual(i, len(EXPECTED_RESULTS)) + + def testnmea(self): # test NMEA parsing, including malformed UNI GLL + EXPECTED_RESULTS = ( + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + ) + + i = 0 + with open(os.path.join(DIRNAME, "pygpsdata_nmea.log"), "rb") as stream: + ubr = UNIReader( + stream, + protfilter=UNI_PROTOCOL | RTCM3_PROTOCOL | NMEA_PROTOCOL, + parsing=True, + parsebitfield=1, + validate=VALCKSUM, quitonerror=ERR_RAISE, ) for raw, parsed in ubr: diff --git a/tests/test_static.py b/tests/test_static.py index 49279aa..63220d1 100644 --- a/tests/test_static.py +++ b/tests/test_static.py @@ -12,7 +12,6 @@ import os import unittest -from datetime import datetime, timezone import pyunigps.exceptions as une import pyunigps.unitypes_core as unt @@ -25,7 +24,6 @@ header2vals, msgname2id, nomval, - utc2wnotow, val2bytes, ) from pyunigps.unitypes_core import CV, UNI_MSGIDS @@ -161,17 +159,6 @@ def testescapeall(self): print(res) self.assertEqual(res, EXPECTED_RESULT) - def testutc2wnotow(self): - dat = datetime(2026, 1, 28, 9, 34, 12, 234000, tzinfo=timezone.utc) - wno, tow, ls = utc2wnotow(dat) - # print(wno, tow, ls) - self.assertEqual((wno, tow), (2403, 293670234)) - wno, tow, ls = utc2wnotow() - # print(wno, tow, ls) - self.assertIsInstance(wno, int) - self.assertIsInstance(tow, int) - self.assertIsInstance(ls, int) - def testheader2bytes(self): t = header2bytes(msgid=17, length=308, cpuidle=0, wno=2406, tow=34675834) print(t)