From 78f6316f21f7384095cdc7fa9c9862b8fd782611 Mon Sep 17 00:00:00 2001 From: SEMU Admin <28569967+semuadmin@users.noreply.github.com> Date: Tue, 20 Oct 2026 11:19:16 +0100 Subject: [PATCH] update sbf handler --- .vscode/settings.json | 7 +- .vscode/tasks.json | 18 +- INSTALLATION.md | 2 +- README.md | 2 +- RELEASE_NOTES.md | 6 + examples/F9P_3D_printed_case.md | 5 - examples/Sparkfun_GNSS_case_3Dprinter.md | 9 + pyproject.toml | 18 +- src/pygpsclient/_version.py | 2 +- src/pygpsclient/gnss_status.py | 1 + src/pygpsclient/helpers.py | 81 ++++--- src/pygpsclient/init_presets.py | 7 +- src/pygpsclient/levelsview_frame.py | 22 +- src/pygpsclient/nmea_handler.py | 2 + src/pygpsclient/qgc_handler.py | 26 +-- src/pygpsclient/rover_frame.py | 11 +- src/pygpsclient/sbf_handler.py | 255 ++++++++++++++++++++--- src/pygpsclient/skyview_frame.py | 2 +- src/pygpsclient/stream_handler.py | 10 +- src/pygpsclient/ubx_handler.py | 127 +++++------ src/pygpsclient/uni_handler.py | 70 ++++--- tests/test_static.py | 25 ++- 22 files changed, 501 insertions(+), 207 deletions(-) delete mode 100644 examples/F9P_3D_printed_case.md create mode 100644 examples/Sparkfun_GNSS_case_3Dprinter.md diff --git a/.vscode/settings.json b/.vscode/settings.json index a1d903ca..aacce995 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -23,9 +23,12 @@ "python.analysis.addHoverSummaries": false, "python-envs.defaultEnvManager": "ms-python.python:venv", "python.venvPath": "${userHome}/pygpsclient/bin/python3", - "python.systemPath": "/usr/local/bin/python3.15t", // free-threading version - //"python.systemPath": "/usr/local/bin/python3.10", // earliest supported version + //"python.defaultInterpreterPath": "${userHome}/pygpsclient/Scripts/python", // Windows + //"python.systemPath": "C:/Program Files/Python315/python.exe", // latest supported version Windows "python.defaultInterpreterPath": "${userHome}/pygpsclient/bin/python3", + "python.systemPath": "/usr/local/bin/python3.15", // latest supported version + //"python.systemPath": "/usr/local/bin/python3.15t", // free-threading version + //"python.systemPath": "/usr/local/bin/python3.10", // earliest supported version "python-envs.pythonProjects": [ { "path": ".", diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 4388f784..6c02465b 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -21,6 +21,17 @@ "-rf", "${config:venv}", ], + "windows": { + "command": "Remove-Item", + "args": [ + "-Path", + "${config:venv}", + "-Recurse", + "-Force", + "-ErrorAction", + "SilentlyContinue" // doesn't work - exit code 1 + ], + }, "problemMatcher": [] }, { @@ -45,6 +56,9 @@ "args": [ "~/pygpsclient/bin/activate", ], + "windows": { + "command": "~/pygpsclient/Scripts/activate" + }, "problemMatcher": [], }, { @@ -119,9 +133,9 @@ "-Recurse", "-Force", "-ErrorAction", - "SilentlyContinue", // doesn't work! - stops on exit code 1 anyway + "SilentlyContinue", // doesn't work - exit code 1 "-Path", - "'build','dist','htmlcov','docs/_build','src/${config:modulename}.egg-info'", + "'dist'", ] }, "options": { diff --git a/INSTALLATION.md b/INSTALLATION.md index 5e413960..ac81a3b1 100644 --- a/INSTALLATION.md +++ b/INSTALLATION.md @@ -57,7 +57,7 @@ The exact location of the site_packages and binary directories will depend on th ### free-threading vs global interpreter lock (GIL) -For various technical reasons, Python has historically implemented what is known as a [global interpreter lock (GIL)](https://docs.python.org/3/glossary.html#term-global-interpreter-lock), which effectively means only one thread can execute Python [byte-code](https://docs.python.org/3/glossary.html#term-bytecode) at a time. Python versions since 13.3 include *optional* support for [free-threading](https://docs.python.org/3/howto/free-threading-python.html) whereby the GIL is disabled, allowing genuine thread concurrency on multi-core machines (**[with some caveats](https://docs.python.org/3/howto/free-threading-python.html#known-limitations)**). Free-threading support has become progressively more mature with Python versions 3.14 and 3.15. +For various technical reasons, Python has historically implemented what is known as a [global interpreter lock (GIL)](https://docs.python.org/3/glossary.html#term-global-interpreter-lock), which effectively means only one thread can execute Python [byte-code](https://docs.python.org/3/glossary.html#term-bytecode) at a time. Python versions since 13.3 include *optional* support for [free-threading](https://docs.python.org/3/howto/free-threading-python.html) whereby the GIL is disabled, allowing genuine thread concurrency on multi-core machines (**[with some caveats](https://docs.python.org/3/howto/free-threading-python.html#known-limitations)**). Free-threading support has become progressively more mature with Python versions 3.14 and 3.15, though it still involves some performance trade-offs (**running PyGPSClient under a free-threading interpreter results in a performance *decrease* of around 4-5%**). To utilise the free-threading version of Python, append the letter 't' to the Python version, e.g.: ```shell diff --git a/README.md b/README.md index 57b36c9b..9e29bcc2 100644 --- a/README.md +++ b/README.md @@ -203,7 +203,7 @@ For more comprehensive installation instructions, please refer to [INSTALLATION. --- | Widget | To show or hide the various widgets, go to Menu..View and click on the relevant hide/show option. | |---------------------------|---------------------------------------------------------------------------------------------------| -|![banner widget](https://github.com/semuconsulting/PyGPSClient/blob/main/images/banner_widget.png?raw=true)| Expandable banner showing key navigation status information based on messages received from receiver. To expand or collapse the banner or serial port configuration widgets, click the ![expand icon](https://github.com/semuconsulting/PyGPSClient/blob/main/src/pygpsclient/resources/iconmonstr-arrow-80-16.png?raw=true)/![expand icon](https://github.com/semuconsulting/PyGPSClient/blob/main/src/pygpsclient/resources/iconmonstr-triangle-1-16.png?raw=true) buttons. Double-click the "hae" label or value to toggle between hae (height above ellipsoid) and sep (separation) values. **NB**: some fields (e.g. hdop/vdop, hacc/vacc) are only available from proprietary NMEA or UBX messages and may not be output by default. The minimum messages required to populate all available fields are: NMEA: GGA, GSA, GSV, RMC, UBX00 (proprietary); UBX: NAV-DOP, NAV-PVT, NAV-SAT; UNI: BESTNAV, SATSINFO, STADOP. | +|![banner widget](https://github.com/semuconsulting/PyGPSClient/blob/main/images/banner_widget.png?raw=true)| Expandable banner showing key navigation status information based on messages received from receiver. To expand or collapse the banner or serial port configuration widgets, click the ![expand icon](https://github.com/semuconsulting/PyGPSClient/blob/main/src/pygpsclient/resources/iconmonstr-arrow-80-16.png?raw=true)/![expand icon](https://github.com/semuconsulting/PyGPSClient/blob/main/src/pygpsclient/resources/iconmonstr-triangle-1-16.png?raw=true) buttons. Double-click the "hae" label or value to toggle between hae (height above ellipsoid) and sep (separation) values. **NB**: some fields (e.g. hdop/vdop, hacc/vacc) are only available from proprietary messages and may not be output by default. The minimum messages required to populate all available fields are: NMEA: GGA, GSA, GSV, RMC + UBX00 (u-blox proprietary); UBX (u-blox): NAV-DOP, NAV-PVT, NAV-SAT; SBF (Septentrio): PVTGeodetic/PVTCartesian, DOP + NMEA GSV; UNI (Unicore): BESTNAV, SATSINFO, STADOP | |![console widget](https://github.com/semuconsulting/PyGPSClient/blob/main/images/console_widget.png?raw=true)| Configurable serial console widget showing incoming GNSS data streams in either parsed, binary or tabular hexadecimal formats. Click ![clipboard icon](https://github.com/semuconsulting/PyGPSClient/blob/main/src/pygpsclient/resources/iconmonstr-clipboard-13-24.png?raw=true) to copy contents to the clipboard. The scroll behaviour and number of lines retained in the console can be configured via the settings panel. Supports user-configurable color tagging of selected strings for easy identification. Color tags are loaded from the `"colortag_b":` value (`0` = disable, `1` = enable) and `"colortags_l":` list (`[string, color]` pairs) in your json configuration file (see example provided). If color is set to "HALT", streaming will halt on any match and a warning displayed. NB: color tagging does impose a small performance overhead - turning it off will improve console response times at very high transaction rates.| |![skyview widget](https://github.com/semuconsulting/PyGPSClient/blob/main/images/skyview_widget.png?raw=true)| Skyview widget showing current satellite visibility and position (elevation / azimuth). Satellite icon borders are colour-coded to distinguish between different GNSS constellations. For consistency between NMEA and UBX data sources, will display GLONASS NMEA SVID (65-96) rather than slot (1-24). A manually-edited boolean configuration parameter `unusedsat_b` determines whether or not to include satellites where CNo = 0; default = 0. | |![levelsview widget](https://github.com/semuconsulting/PyGPSClient/blob/main/images/graphview_widget.png?raw=true)| Levels view widget showing current satellite carrier-to-noise (C/No) levels for each GNSS constellation. Double-click to toggle legend.| diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index a17d4352..b79f6c44 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,5 +1,11 @@ # PyGPSClient Release Notes +### RELEASE 1.7.9 + +1. Enhance Septentrio SBF message handling. +1. Update minimum dependency versions. +1. Add Python 3.15 to workflows. + ### RELEASE 1.7.8 1. Fix `KeyError` issue when using App Config dialog to update chart `numchan` setting in older config files. diff --git a/examples/F9P_3D_printed_case.md b/examples/F9P_3D_printed_case.md deleted file mode 100644 index da0ba29f..00000000 --- a/examples/F9P_3D_printed_case.md +++ /dev/null @@ -1,5 +0,0 @@ -## Simple enclosure for SparkFun GPS-RTK-SMA ZED-F9P Breakout - -https://www.thingiverse.com/thing:6774791 - -![3D printed case](https://cdn.thingiverse.com/assets/2d/79/53/64/a3/large_display_f9pcase10.png) diff --git a/examples/Sparkfun_GNSS_case_3Dprinter.md b/examples/Sparkfun_GNSS_case_3Dprinter.md new file mode 100644 index 00000000..2ffea67b --- /dev/null +++ b/examples/Sparkfun_GNSS_case_3Dprinter.md @@ -0,0 +1,9 @@ +## Simple enclosure for SparkFun GPS-RTK-SMA Breakout Boards - u-blox ZED-F9P & ZED-X20P and Septentrio Mosaic G5 P3 + +https://www.thingiverse.com/thing:6774791 + +Will also fit other Sparkfun GNSS boards with similar PCB template. + +![3D printed case STL](https://cdn.thingiverse.com/assets/2d/79/53/64/a3/large_display_f9pcase10.png) + +![3D printed case](https://resize.thingiverse.com/?url=https://cdn.thingiverse.com/assets/4c/05/93/50/74/IMG_3033.jpg&w=600&h=450&fit=inside&n=-1) diff --git a/pyproject.toml b/pyproject.toml index 8b6c6225..51c94c7f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -60,7 +60,21 @@ classifiers = [ "Topic :: Scientific/Engineering :: GIS", ] -dependencies = ["requests>=2.34.0", "Pillow>=12.0.0", "pygnssutils>=1.2.9"] +dependencies = [ + "certifi>=2026.0.0", + "pillow>=12.0.0", + "pygnssutils>=1.3.0", + "pynmeagps>=1.1.8", + "pyqgc>=1.0.1", + "pyrtcm>=1.2.1", + "pysbf2>=1.0.7", + "pyserial>=3.5", + "pyspartn>=1.1.0", + "pyubx2>=1.3.9", + "pyubxutils>=1.0.6", + "pyunigps>=1.0.1", + "requests>=2.34.0", +] [project.scripts] pygpsclient = "pygpsclient.__main__:main" @@ -72,7 +86,7 @@ repository = "https://github.com/semuconsulting/PyGPSClient" changelog = "https://github.com/semuconsulting/PyGPSClient/blob/main/RELEASE_NOTES.md" [dependency-groups] -optional = ["rasterio"] +optional = ["rasterio", "psutil"] build = [ "awscli", "build", diff --git a/src/pygpsclient/_version.py b/src/pygpsclient/_version.py index 6dd8c95d..728cc9d1 100644 --- a/src/pygpsclient/_version.py +++ b/src/pygpsclient/_version.py @@ -8,4 +8,4 @@ :license: BSD 3-Clause """ -__version__ = "1.7.8" +__version__ = "1.7.9" diff --git a/src/pygpsclient/gnss_status.py b/src/pygpsclient/gnss_status.py index 98d5daca..cb7361a1 100644 --- a/src/pygpsclient/gnss_status.py +++ b/src/pygpsclient/gnss_status.py @@ -49,6 +49,7 @@ def reset(self): self.pdop = 0.0 # dilution of precision DOP self.hdop = 0.0 # horizontal DOP self.vdop = 0.0 # vertical DOP + self.tdop = 0.0 # time DOP self.hacc = 0.0 # horizontal accuracy m self.vacc = 0.0 # vertical accuracy m self.diff_corr = 0 # DGPS correction status True/False diff --git a/src/pygpsclient/helpers.py b/src/pygpsclient/helpers.py index aef01bc9..9e35ae8b 100644 --- a/src/pygpsclient/helpers.py +++ b/src/pygpsclient/helpers.py @@ -38,8 +38,8 @@ Tk, font, ) -from types import FunctionType, MethodType, NoneType -from typing import Any, Literal +from types import NoneType +from typing import Any, Callable, Literal from pygnssutils import version as PGVERSION from pynmeagps import WGS84_SMAJ_AXIS, NMEAMessage @@ -129,7 +129,7 @@ def validate( low: int | float = MINFLOAT, high: int | float = MAXFLOAT, regex: str | NoneType = None, - func: MethodType | FunctionType | NoneType = None, + func: Callable | NoneType = None, args: tuple = (), ) -> bool: """ @@ -142,7 +142,7 @@ def validate( bits for VALSIGNED/VALUNSIGNED :param int | float high: optional max value :param str | NoneType regex: regex expression - :param MethodType | FunctionType | NoneType func: custom validation function + :param Callable | NoneType func: custom validation function :param Any args: optional function arguments :return: True/False @@ -205,7 +205,7 @@ def validate( def trace_update( self: IntVar | StringVar | DoubleVar | BooleanVar, mode: Literal["array", "read", "write", "unset"], - callback: object, + callback: Callable, add: bool = True, ) -> str | NoneType: """ @@ -258,24 +258,6 @@ def area_in_bounds( ) -def bitsval(bitfield: bytes, position: int, length: int) -> int: - """ - Get unisgned integer value of masked bits in bitfield. - - :param bytes bitfield: bytes - :param int position: position in bitfield, from leftmost bit - :param int length: length of masked bits - :return: value - :rtype: int - """ - - lbb = len(bitfield) * 8 - if position + length > lbb: - return None - - return int.from_bytes(bitfield, "big") >> (lbb - position - length) & 2**length - 1 - - def brew_installed() -> bool: """ Check if Python installed under Homebrew. @@ -1012,6 +994,41 @@ def rgb2str(r: int, g: int, b: int) -> str: return f"#{r:02x}{g:02x}{b:02x}" +def sbfsvid2svid(svid: int) -> tuple[int, int]: + """ + Convert Septentrio SVID to standardised SVID + + :return tuple of gnssid, svid + :rtype: tuple[int, int] + """ + + if 1 <= svid <= 37: + return (0, svid) + if 38 <= svid <= 61: + return (6, svid - 37) + if 63 <= svid <= 68: + return (6, svid - 38) + if 71 <= svid <= 106: + return (2, svid - 70) + if 120 <= svid <= 140: + return (1, svid - 100) + if 141 <= svid <= 180: + return (3, svid - 140) + if 181 <= svid <= 190: + return (5, svid - 180) + if 191 <= svid <= 197: + return (7, svid - 190) + if 198 <= svid <= 215: + return (1, svid - 157) + if 216 <= svid <= 222: + return (7, svid - 208) + if 223 <= svid <= 245: + return (3, svid - 182) + if 250 <= svid <= 251: + return (1, svid - 212) + return 0, 0 + + def scale_font( width: int, basesize: int, @@ -1216,28 +1233,28 @@ def stringvar2val(val: str, att: str) -> Any: :rtype: Any """ + valt = val if att in ("DE", "LA", "LN"): # NMEA float - val = float(val) + valt = float(val) elif att in ("CH", "DT", "HX", "LAD", "LND", "TM"): # NMEA str, hex pass elif att == "IN": # NMEA int - val = int(val) + valt = int(val) elif atttyp(att) in ("E", "I", "L", "U"): # integer if val.find(".") != -1: # ignore scaling decimals - val = val[0 : val.find(".")] - val = int(val) + valt = val[0 : val.find(".")] + valt = int(valt) elif atttyp(att) == "X": # bytes if val[0:2] in ("0x", "0X"): # allow for hex representation mod = 16 else: mod = 10 - val = int(val, mod).to_bytes(attsiz(att), "big") + valt = int(val, mod).to_bytes(attsiz(att), "big") elif atttyp(att) == "C": # char - val = bytes(val, "utf-8") + valt = bytes(val, "utf-8") elif atttyp(att) == "R": # float - val = float(val) - - return val + valt = float(val) + return valt def svid2gnssid(svid) -> int: diff --git a/src/pygpsclient/init_presets.py b/src/pygpsclient/init_presets.py index 369bcb0d..cc60c5e4 100644 --- a/src/pygpsclient/init_presets.py +++ b/src/pygpsclient/init_presets.py @@ -113,20 +113,21 @@ "Septentrio List Command Help; help", "Septentrio Enable NMEA messages; sno, Stream1, COM1, GGA+GSA+GLL+GSV+RMC+VTG, sec1", "Septentrio Disable NMEA messages; sno, Stream1, none, none, off", - "Septentrio Enable PVT Group stream; ssgp, Group1, MeasEpoch+PVTCartesian+DOP+DiffCorr; sso, Stream2, COM1, Group1, sec1", + "Septentrio Enable PVT Group stream; ssgp, Group1, MeasEpoch+PVTGeodetic+DOP+DiffCorr; sso, Stream2, COM1, Group1, sec1", "Septentrio Disable PVT Group stream; sso, Stream2, none, none, off", "Septentrio Enable RAW Group stream; ssgp, Group2, RawNavBits; sso, Stream3, COM1, Group2, sec1", "Septentrio Disable RAW Group stream; sso, Stream3, none, none, off", "Septentrio Enable RINEX Group stream; ssgp, Group3, Rinex; sso, Stream4, COM1, Group3, sec1", "Septentrio Disable RINEX Group stream; sso, Stream4, none, none, off", "Septentrio Output next Measurement Epoch; esoc, COM1, MeasEpoch", - "Septentrio Enable PVTGeod stream; sso, Stream2, COM1, PVTGeod, sec1", - "Septentrio Disable PVTGeod stream; sso, Stream2, COM1, none, off", + "Septentrio Enable PVTCartesian stream; sso, Stream2, COM1, PVTCartesian, sec1", + "Septentrio Disable PVTCartesian stream; sso, Stream2, COM1, none, off", "Septentrio Enable Status stream; sso, Stream3, COM1, Status, sec1", "Septentrio Disable Status stream; sso, Stream3, COM1, none, off", "Septentrio Set Fixed Base Station Mode; setDataInOut,COM1, ,RTCMv3;setRTCMv3Formatting,1234;setStaticPosGeodetic,Geodetic1,37.23345,-115.81513,15;setPVTMode,Static, ,Geodetic1", "Septentrio Set Survey-In Base Station Mode;setDataInOut,COM1, ,RTCMv3;setRTCMv3Formatting,1234;setPVTMode,Static, ,auto", "Septentrio Stop RTCM output;setDataInOut,COM1, ,none", + "Septentrio Enable Galileo HAS; setSignalTracking, +GALE6BC;setSignalUsage, ,+GALE6BC;setPVTMode, , +PPP", "Unicore UM98n Restore Factory Defaults CONFIRM; freset", "Unicore UM98n Save Current Configuration to NVM CONFIRM; saveconfig", "Unicore UM98n Force COLD reset CONFIRM; reset", diff --git a/src/pygpsclient/levelsview_frame.py b/src/pygpsclient/levelsview_frame.py index 07c7447f..e27914d3 100644 --- a/src/pygpsclient/levelsview_frame.py +++ b/src/pygpsclient/levelsview_frame.py @@ -16,7 +16,7 @@ import logging from copy import deepcopy -from tkinter import NE, NSEW, Frame, Tk, font +from tkinter import CENTER, NE, NSEW, Frame, Tk, font from pygpsclient.custom_classes import ( TAG_DATA, @@ -182,9 +182,13 @@ def update_frame(self): offset = self._canvas.xoffl colwidth = (w - self._canvas.xoffl - self._canvas.xoffr + 1) / siv xfnt, _, _, _ = fitfont(XLBLFMT, colwidth, self._canvas.yoffb, XLBLANGLE) - for gnssId, svid, _, _, cno, _ in sorted( - data.values() - ): # sort by ascending gnssid, svid + nfont = font.Font(size=int(min(self.width, self.height) / 30)) + nominal = False + # sort by ascending gnssid, svid + for gnssId, svid, _, _, cno, _ in sorted(data.values()): + if cno < 0: # nominal C/No + cno = abs(cno) + nominal = True snr_y = int(cno) * (h - self._canvas.yoffb - 1) / MAX_SNR _, ol_col = GNSS_LIST[gnssId] self._canvas.create_rectangle( @@ -207,6 +211,16 @@ def update_frame(self): anchor=NE, tags=TAG_DATA, ) + if nominal: + self._canvas.create_text( + self._canvas.width / 2, + self._canvas.height / 1.5, + text="C/No NOMINAL", + font=nfont, + fill=FGCOL, + anchor=CENTER, + tags=TAG_DATA, + ) offset += colwidth except ValueError: # some values can be null on startup pass diff --git a/src/pygpsclient/nmea_handler.py b/src/pygpsclient/nmea_handler.py index 329fc0c7..a21f1540 100644 --- a/src/pygpsclient/nmea_handler.py +++ b/src/pygpsclient/nmea_handler.py @@ -132,6 +132,8 @@ def process_data(self, raw_data: bytes, parsed_data: NMEAMessage): self._process_AIR(parsed_data) except ValueError: pass + except Exception as err: # pylint: disable=broad-exception-caught + self.logger.exception(err) def _process_RMC(self, data: NMEAMessage): """ diff --git a/src/pygpsclient/qgc_handler.py b/src/pygpsclient/qgc_handler.py index ad0750cd..b8bc118c 100644 --- a/src/pygpsclient/qgc_handler.py +++ b/src/pygpsclient/qgc_handler.py @@ -3,10 +3,10 @@ QGC Protocol handler - handles all incoming QGC messages -Parses individual QGC messages (using pyqgc library) -and adds selected attribute values to the app.gnss_status -data dictionary. This dictionary is then used to periodically -update the various user-selectable widget panels. +Parses individual QGC (proprietary Quectel binary) messages +(using pyqgc library) and adds selected attribute values to +the app.gnss_status data dictionary. This dictionary is then +used to periodically update the various user-selectable widget panels. Created on 6 Oct 2025 @@ -47,11 +47,13 @@ def process_data(self, raw_data: bytes, parsed_data: object): :param QGCMessage parsed_data: parsed data """ - if raw_data is None: - pass - if self.__app.gnss_status.version_data["hwversion"] == NA: - self.__app.gnss_status.version_data["hwversion"] = "Quectel" - self.__app.set_device_label( - self.__app.gnss_status.version_data["hwversion"] - ) - # self.logger.debug(f"data received {parsed_data.identity}") + try: + if raw_data is None: + pass + if self.__app.gnss_status.version_data["hwversion"] == NA: + self.__app.gnss_status.version_data["hwversion"] = "Quectel" + self.__app.set_device_label( + self.__app.gnss_status.version_data["hwversion"] + ) + except Exception as err: # pylint: disable=broad-exception-caught + self.logger.exception(err) diff --git a/src/pygpsclient/rover_frame.py b/src/pygpsclient/rover_frame.py index f2369970..ee8ce796 100644 --- a/src/pygpsclient/rover_frame.py +++ b/src/pygpsclient/rover_frame.py @@ -94,7 +94,7 @@ def _body(self): self._lbl_options = Label(self._frm_options, text="Source", fg=PNTCOL, bg=BGCOL) self._spn_options = Spinbox( self._frm_options, - values=["NAV-RELPOSNED", "NAV-DAHEADING", RTCM56], + values=["UBX NAV-RELPOSNED", "UBX NAV-DAHEADING", RTCM56, "SBF BaseVector"], width=16, wrap=True, background=BGCOL, @@ -227,7 +227,14 @@ def update_frame(self): diffsoln = {0: "NO DGPS", 1: "DGPS"}.get(diffsoln, "DGPS NA") valrp = {0: "INVALID RP", 1: "VALID RP"}.get(valrp, "RP NA") valhdg = {0: "INVALID HDG", 1: "VALID HDG"}.get(valhdg, "HDG NA") - carrsoln = {0: "NO RTK", 1: "RTK FLOAT", 2: "RTK FIXED"}.get(carrsoln, "RTK NA") + carrsoln = { + 0: "NO RTK", + 1: "RTK FLOAT", + 2: "RTK FIXED", + 3: "DGPS", + 4: "SBAS", + 5: "PPP", + }.get(carrsoln, "NA") moving = {0: "STATIC", 1: "MOVING"}.get(moving, "MOVING NA") self._canvas.create_text( INSET, diff --git a/src/pygpsclient/sbf_handler.py b/src/pygpsclient/sbf_handler.py index c3b153f6..a487d06c 100644 --- a/src/pygpsclient/sbf_handler.py +++ b/src/pygpsclient/sbf_handler.py @@ -3,10 +3,10 @@ SBF Protocol handler - handles all incoming SBF messages -Parses individual UBX messages (using pysbf2 library) -and adds selected attribute values to the app.gnss_status -data dictionary. This dictionary is then used to periodically -update the various user-selectable widget panels. +Parses individual SBF (proprietary Septentrio binary) messages +(using pysbf2 library) and adds selected attribute values to +the app.gnss_status data dictionary. This dictionary is then +used to periodically update the various user-selectable widget panels. Created on 19 May 2025 @@ -17,15 +17,26 @@ import logging from math import degrees +from time import time +from pynmeagps import bearing, ecef2llh, haversine, llh2ecef from pysbf2 import SBFMessage, itow2utc from pygpsclient.dialog_state import DLGTTTY from pygpsclient.globals import ASCII, BSR -from pygpsclient.helpers import fix2desc +from pygpsclient.helpers import fix2desc, hdg2yaw, sbfsvid2svid from pygpsclient.strings import NA -DNUL = -20000000000 # -2 * (10**10) +CARRSOLN = { + 4: 2, # RTK-FIXED + 7: 2, # RTK-FIXED + 5: 1, # RTK-FLOAT + 8: 1, # RTK-FLOAT + 2: 3, # RTK + 6: 4, # SBAS + 10: 5, # PPP +} +DNUL = -20000000000 # -2 * 1e10 DNUS = 65535 @@ -58,26 +69,39 @@ def process_data(self, raw_data: bytes, parsed_data: object): :param UBXMessage parsed_data: parsed data """ - if raw_data is None: - return - if self.__app.gnss_status.version_data["hwversion"] == NA: - self.__app.gnss_status.version_data["hwversion"] = "Septentrio" - self.__app.set_device_label( - self.__app.gnss_status.version_data["hwversion"] - ) - # self.logger.debug(f"data received {parsed_data.identity}") - if parsed_data.identity == "PVTGeodetic": - self._process_PVTGeodetic(parsed_data) - elif parsed_data.identity == "ReceiverStatus": - self._process_ReceiverStatus(parsed_data) - elif parsed_data.identity == "ReceiverSetup": - self._process_ReceiverSetup(parsed_data) + try: + if raw_data is None: + return + if self.__app.gnss_status.version_data["hwversion"] == NA: + self.__app.gnss_status.version_data["hwversion"] = "Septentrio" + self.__app.set_device_label( + self.__app.gnss_status.version_data["hwversion"] + ) + # self.logger.debug(f"data received {parsed_data.identity}") + if parsed_data.identity in ("PVTGeodetic", "NavGeod"): + self._process_PVT_geodetic(parsed_data) + elif parsed_data.identity in ("PVTCartesian", "NavCart"): + self._process_PVT_cartesian(parsed_data) + elif parsed_data.identity == "DOP": + self._process_DOP(parsed_data) + elif parsed_data.identity == "SatVisibility": + self._process_SatVisibility(parsed_data) + elif parsed_data.identity == "ReceiverStatus": + self._process_ReceiverStatus(parsed_data) + elif parsed_data.identity == "ReceiverSetup": + self._process_ReceiverSetup(parsed_data) + elif parsed_data.identity == "AttEuler": + self._process_AttEuler(parsed_data) + elif parsed_data.identity in ("BaseVectorGeod", "BaseVectorCart"): + self._process_BaseVector(parsed_data) + except Exception as err: # pylint: disable=broad-exception-caught + self.logger.exception(err) - def _process_PVTGeodetic(self, data: SBFMessage): + def _process_PVT_geodetic(self, data: SBFMessage): """ - Process PVTGeodetic sentence - Position Velocity Track Geodetic. + Process PVT Geodetic sentence - Position Velocity Track Geodetic. - :param SBFMessage data: PVTGeodetic message + :param SBFMessage data: PVT Geodetic message """ self.__app.gnss_status.utc = itow2utc(data.TOW) # datetime.time @@ -85,20 +109,99 @@ def _process_PVTGeodetic(self, data: SBFMessage): self.__app.gnss_status.lat = degrees(data.Latitude) if data.Longitude != DNUL: self.__app.gnss_status.lon = degrees(data.Longitude) - if data.Height != DNUL: + if data.Height != DNUL: # HAE self.__app.gnss_status.hae = data.Height # meters - if data.HAccuracy != DNUS: - self.__app.gnss_status.hacc = data.HAccuracy / 100 # meters - if data.VAccuracy != DNUS: - self.__app.gnss_status.vacc = data.VAccuracy / 100 # meters + if DNUL not in (data.Height, data.Undulation): # SEP + self.__app.gnss_status.alt = data.Height - data.Undulation # meters + self._process_PVT_common(data) + + def _process_PVT_cartesian(self, data: SBFMessage): + """ + Process PVT Cartesian sentence - Position Velocity Track Cartesian. + + :param SBFMessage data: PVT Cartesian message + """ + + self.__app.gnss_status.utc = itow2utc(data.TOW) # datetime.time + if DNUL not in (data.X, data.Y, data.Z): + lat, lon, hae = ecef2llh(data.X, data.Y, data.Z) + self.__app.gnss_status.lat = lat + self.__app.gnss_status.lon = lon + self.__app.gnss_status.hae = hae + if data.Undulation != DNUL: # SEP + self.__app.gnss_status.alt = hae - data.Undulation # meters + self._process_PVT_common(data) + + def _process_PVT_common(self, data: SBFMessage): + """ + Process common elements of PVT Geodetic and Cartesian messages. + + :param SBFMessage data: PVT Geodetic/Cartesian message + """ + if data.NrSV != 255: self.__app.gnss_status.sip = data.NrSV if data.COG != DNUL: self.__app.gnss_status.track = data.COG self.__app.gnss_status.fix = fix2desc("PVTGeodetic", data.Type) if data.MeanCorrAge != 0: - self.__app.gnss_status.diff_age = data.MeanCorrAge / 100 # seconds + self.__app.gnss_status.diff_age = data.MeanCorrAge # seconds self.__app.gnss_status.diff_station = data.ReferenceID + if data.identity in ("NavCart", "NavGeod"): + if data.PosHAcc != DNUS / 1000: + self.__app.gnss_status.hacc = data.PosHAcc # meters + if data.PosVAcc != DNUS / 1000: + self.__app.gnss_status.vacc = data.PosVAcc # meters + if data.PDOP != 0: + self.__app.gnss_status.pdop = data.PDOP + else: # PVTCartesian, PVTGeodetic + if data.HAccuracy != DNUS / 100: + self.__app.gnss_status.hacc = data.HAccuracy # meters + if data.VAccuracy != DNUS / 100: + self.__app.gnss_status.vacc = data.VAccuracy # meters + + def _process_DOP(self, data: SBFMessage): + """ + Process DOP sentence - Degrees of Precision. + + :param SBFMessage data: DOP message + """ + + if data.PDOP != 0: + self.__app.gnss_status.pdop = data.PDOP + if data.HDOP != 0: + self.__app.gnss_status.hdop = data.HDOP + if data.VDOP != 0: + self.__app.gnss_status.vdop = data.VDOP + if data.TDOP != 0: + self.__app.gnss_status.tdop = data.TDOP + + def _process_SatVisibility(self, data: SBFMessage): + """ + Process SatVisibility sentence - Satellite visibility. + + :param SBFMessage data: SatVisibility message + """ + + self.__app.gnss_status.gsv_data = {} + num_siv = int(data.N) + now = time() + + for i in range(num_siv): + idx = f"_{i+1:02d}" + gnssId, svid = sbfsvid2svid(getattr(data, "SVID" + idx)) + elev = getattr(data, "Elevation" + idx) + azim = getattr(data, "Azimuth" + idx) + self.__app.gnss_status.gsv_data[(gnssId, svid)] = ( + gnssId, + svid, + elev, + azim, + -30, # NB: nominal value as cno not provided + now, + ) + + self.__app.gnss_status.siv = len(self.__app.gnss_status.gsv_data) def _process_ReceiverStatus(self, data: SBFMessage): """ @@ -140,6 +243,100 @@ def _process_ReceiverSetup(self, data: SBFMessage): self.__app.gnss_status.version_data["gnss"] = NA self.__app.set_device_label(self.__app.gnss_status.version_data["hwversion"]) + def _process_AttEuler(self, data: SBFMessage): + """ + Process AttEuler sentence. + + :param SBFMessage data: AttEuler message + """ + + ims = self.__app.gnss_status.imu_data + ims["source"] = data.identity + dnu = False + if data.Roll == DNUL: + ims["roll"] = 0 + dnu = True + else: + ims["roll"] = round(data.Roll, 4) + if data.Pitch == DNUL: + ims["pitch"] = 0 + dnu = True + else: + ims["pitch"] = round(data.Pitch, 4) + if data.Heading == DNUL: + ims["yaw"] = 0 + dnu = True + else: + ims["yaw"] = round(hdg2yaw(data.Heading), 4) + ims["status"] = "DONOTUSE" if dnu else f"INFO: {data.Info}" + + def _process_BaseVector(self, data: SBFMessage): + """ + Process BaseVector sentence - Relative position of rover. + + :param SBFMessage data: BaseVector parsed message + """ + + if self.__app.configuration.get("relposnedsettings_d")[ + "source_s" + ] != "SBF BaseVector" or ( + self.__app.gnss_status.lat == 0 and self.__app.gnss_status.lon == 0 + ): + return + + X1, Y1, Z1 = llh2ecef( + self.__app.gnss_status.lat, + self.__app.gnss_status.lon, + self.__app.gnss_status.hae, + ) + + self.__app.gnss_status.rel_pos_source = "SBF BaseVector" + self.__app.gnss_status.acc_heading = 0 + self.__app.gnss_status.acc_length = 0 + + if data.N == 0: + self.__app.gnss_status.rel_pos_heading = 0 + self.__app.gnss_status.rel_pos_length = 0 + self.__app.gnss_status.rel_pos_flags = [0] * 9 + return + + if data.identity == "BaseVectorGeod": + if DNUL in (data.DeltaEast_01, data.DeltaNorth_01, data.DeltaUp_01): + return + X2, Y2, Z2 = ( + data.DeltaEast_01 + X1, + data.DeltaNorth_01 + Y1, + data.DeltaUp_01 + Z1, + ) + else: # BaseVectorCart + if DNUL in (data.DeltaX_01, data.DeltaY_01, data.DeltaZ_01): + return + X2, Y2, Z2 = data.DeltaX_01 + X1, data.DeltaY_01 + Y1, data.DeltaZ_01 + Z1 + + lat2, lon2, _ = ecef2llh(X2, Y2, Z2) + distance = haversine( + self.__app.gnss_status.lat, self.__app.gnss_status.lon, lat2, lon2 + ) + heading = bearing( + lat2, lon2, self.__app.gnss_status.lat, self.__app.gnss_status.lon + ) + if data.identity == "BaseVectorGeod": + heading = (heading - 90) % 360 + self.__app.gnss_status.rel_pos_heading = heading + self.__app.gnss_status.rel_pos_length = distance * 100000 # cm + carrsoln = CARRSOLN.get(data.Type_01, 0) + self.__app.gnss_status.rel_pos_flags = [ + int(data.Error_01 == 0), # gnssFixOK, + int(data.Type_01 in (2, 4, 5, 6, 7, 8, 10)), # diffSoln, + int(data.Error_01 == 0), # relPosValid, + carrsoln, # carrSoln, + int(data.Type_01 in (7, 8)), # isMoving, + 0, # refPosMiss, + 0, # refObsMiss, + int(data.Error_01 == 0), # relPosHeadingValid, + 0, # relPosNormalized, + ] + def process_lif(self, data: str): """ Parse output of lstInternalFile,identification poll, diff --git a/src/pygpsclient/skyview_frame.py b/src/pygpsclient/skyview_frame.py index be0d447d..9d7e92b9 100644 --- a/src/pygpsclient/skyview_frame.py +++ b/src/pygpsclient/skyview_frame.py @@ -134,7 +134,7 @@ def update_frame(self): continue x, y = self._canvas.d2xy(azi, ele) _, ol_col = GNSS_LIST[gnssId] - bg_col = snr2col(cno) + bg_col = snr2col(abs(cno)) self._canvas.create_circle( x, y, diff --git a/src/pygpsclient/stream_handler.py b/src/pygpsclient/stream_handler.py index 853a1d73..cfda40b8 100644 --- a/src/pygpsclient/stream_handler.py +++ b/src/pygpsclient/stream_handler.py @@ -332,13 +332,13 @@ def _readloop( errorhandler=self._errorhandler, ) - raw_data = None + raw_data = b"" parsed_data = None lastevent = datetime.now() while not stopevent.is_set(): if conntype in (CONNECTED, CONNECTED_SOCKET, CONNECTED_FILE): raw_data, parsed_data = ubr.read() - if raw_data is not None: + if raw_data != b"": self._process_message(raw_data, parsed_data) lastevent = datetime.now() else: # timeout or eof @@ -416,9 +416,7 @@ def _readlooptty( self._errorhandler(err) continue - def _process_message( - self, raw_data: bytes | NoneType, parsed_data: Any, marker: str = "" - ): + def _process_message(self, raw_data: bytes, parsed_data: Any, marker: str = ""): """ THREADED PROCESS @@ -426,7 +424,7 @@ def _process_message( Update the gnss_status dictionary with parsed attribute values. - :param bytes | NoneType raw_data: raw message data + :param bytes raw_data: raw message data :param object parsed data: NMEAMessage, UBXMessage or RTCMMessage :param str marker: string prepended to console entries e.g. "NTRIP>>" """ diff --git a/src/pygpsclient/ubx_handler.py b/src/pygpsclient/ubx_handler.py index ea51c33f..7caa9e7b 100644 --- a/src/pygpsclient/ubx_handler.py +++ b/src/pygpsclient/ubx_handler.py @@ -3,10 +3,10 @@ UBX Protocol handler - handles all incoming UBX messages -Parses individual UBX messages (using pyubx2 library) -and adds selected attribute values to the app.gnss_status -data dictionary. This dictionary is then used to periodically -update the various user-selectable widget panels. +Parses individual UBX (proprietary u-blox binary) messages +(using pyubx2 library) and adds selected attribute values +to the app.gnss_status data dictionary. This dictionary is +then used to periodically update the various user-selectable widget panels. Created on 30 Sep 2020 @@ -61,60 +61,63 @@ def process_data(self, raw_data: bytes, parsed_data: object): :param UBXMessage parsed_data: parsed data """ - if raw_data is None: - return - if self.__app.gnss_status.version_data["hwversion"] == NA: - self.__app.gnss_status.version_data["hwversion"] = "u-blox" - self.__app.set_device_label( - self.__app.gnss_status.version_data["hwversion"] - ) - # self.logger.debug(f"data received {parsed_data.identity}") - if parsed_data.identity[0:3] in ("ACK", "CFG"): - self._process_ACK(parsed_data) - elif parsed_data.identity == "ESF-ALG": - self._process_ESF_ALG(parsed_data) - elif parsed_data.identity == "HNR-ATT": - self._process_HNR_ATT(parsed_data) - elif parsed_data.identity == "HNR-PVT": - self._process_HNR_PVT(parsed_data) - elif parsed_data.identity == "NAV-ATT": - self._process_NAV_ATT(parsed_data) - elif parsed_data.identity in ("NAV-DOP", "NAV2-DOP"): - self._process_NAV_DOP(parsed_data) - elif parsed_data.identity in ("NAV-POSLLH", "NAV-HPPOSLLH"): - self._process_NAV_POSLLH(parsed_data) - elif parsed_data.identity in ("NAV-PVT", "NAV2-PVT"): - self._process_NAV_PVT(parsed_data) - elif parsed_data.identity in ("NAV-PVAT", "NAV2-PVAT"): - self._process_NAV_PVAT(parsed_data) - elif parsed_data.identity in ("NAV-RELPOSNED",): - self._process_NAV_RELPOSNED(parsed_data) - elif parsed_data.identity in ("NAV-DAHEADING",): - self._process_NAV_DAHEADING(parsed_data) - elif parsed_data.identity in ("NAV-SAT", "NAV2-SAT"): - self._process_NAV_SAT(parsed_data) - elif parsed_data.identity in ("NAV-SIG", "NAV2-SIG"): - self._process_NAV_SIG(parsed_data) - elif parsed_data.identity in ("NAV-STATUS", "NAV2-STATUS"): - self._process_NAV_STATUS(parsed_data) - elif parsed_data.identity == "NAV-SVIN": - self._process_NAV_SVIN(parsed_data) - elif parsed_data.identity == "NAV-SVINFO": - self._process_NAV_SVINFO(parsed_data) - elif parsed_data.identity == "NAV-SOL": - self._process_NAV_SOL(parsed_data) - elif parsed_data.identity == "NAV-VELNED": - self._process_NAV_VELNED(parsed_data) - elif parsed_data.identity == "MON-COMMS": - self._process_MON_COMMS(parsed_data) - elif parsed_data.identity == "MON-SPAN": - self._process_MON_SPAN(parsed_data) - elif parsed_data.identity == "MON-SYS": - self._process_MON_SYS(parsed_data) - elif parsed_data.identity == "MON-VER": - self._process_MONVER(parsed_data) - elif parsed_data.identity == "RXM-RTCM": - self._process_RXM_RTCM(parsed_data) + try: + if raw_data is None: + return + if self.__app.gnss_status.version_data["hwversion"] == NA: + self.__app.gnss_status.version_data["hwversion"] = "u-blox" + self.__app.set_device_label( + self.__app.gnss_status.version_data["hwversion"] + ) + # self.logger.debug(f"data received {parsed_data.identity}") + if parsed_data.identity[0:3] in ("ACK", "CFG"): + self._process_ACK(parsed_data) + elif parsed_data.identity == "ESF-ALG": + self._process_ESF_ALG(parsed_data) + elif parsed_data.identity == "HNR-ATT": + self._process_HNR_ATT(parsed_data) + elif parsed_data.identity == "HNR-PVT": + self._process_HNR_PVT(parsed_data) + elif parsed_data.identity == "NAV-ATT": + self._process_NAV_ATT(parsed_data) + elif parsed_data.identity in ("NAV-DOP", "NAV2-DOP"): + self._process_NAV_DOP(parsed_data) + elif parsed_data.identity in ("NAV-POSLLH", "NAV-HPPOSLLH"): + self._process_NAV_POSLLH(parsed_data) + elif parsed_data.identity in ("NAV-PVT", "NAV2-PVT"): + self._process_NAV_PVT(parsed_data) + elif parsed_data.identity in ("NAV-PVAT", "NAV2-PVAT"): + self._process_NAV_PVAT(parsed_data) + elif parsed_data.identity in ("NAV-RELPOSNED",): + self._process_NAV_RELPOSNED(parsed_data) + elif parsed_data.identity in ("NAV-DAHEADING",): + self._process_NAV_DAHEADING(parsed_data) + elif parsed_data.identity in ("NAV-SAT", "NAV2-SAT"): + self._process_NAV_SAT(parsed_data) + elif parsed_data.identity in ("NAV-SIG", "NAV2-SIG"): + self._process_NAV_SIG(parsed_data) + elif parsed_data.identity in ("NAV-STATUS", "NAV2-STATUS"): + self._process_NAV_STATUS(parsed_data) + elif parsed_data.identity == "NAV-SVIN": + self._process_NAV_SVIN(parsed_data) + elif parsed_data.identity == "NAV-SVINFO": + self._process_NAV_SVINFO(parsed_data) + elif parsed_data.identity == "NAV-SOL": + self._process_NAV_SOL(parsed_data) + elif parsed_data.identity == "NAV-VELNED": + self._process_NAV_VELNED(parsed_data) + elif parsed_data.identity == "MON-COMMS": + self._process_MON_COMMS(parsed_data) + elif parsed_data.identity == "MON-SPAN": + self._process_MON_SPAN(parsed_data) + elif parsed_data.identity == "MON-SYS": + self._process_MON_SYS(parsed_data) + elif parsed_data.identity == "MON-VER": + self._process_MONVER(parsed_data) + elif parsed_data.identity == "RXM-RTCM": + self._process_RXM_RTCM(parsed_data) + except Exception as err: # pylint: disable=broad-exception-caught + self.logger.exception(err) def _process_ACK(self, msg: UBXMessage): """ @@ -533,7 +536,7 @@ def _process_NAV_RELPOSNED(self, data: UBXMessage): if ( self.__app.configuration.get("relposnedsettings_d")["source_s"] - != data.identity + != "UBX NAV-RELPOSNED" ): return @@ -555,7 +558,7 @@ def _process_NAV_RELPOSNED(self, data: UBXMessage): data.accHeading, ) - self.__app.gnss_status.rel_pos_source = data.identity + self.__app.gnss_status.rel_pos_source = "UBX NAV-RELPOSNED" self.__app.gnss_status.rel_pos_heading = relPosHeading self.__app.gnss_status.rel_pos_length = relPosLength self.__app.gnss_status.acc_heading = accHeading @@ -585,7 +588,7 @@ def _process_NAV_DAHEADING(self, data: UBXMessage): if ( self.__app.configuration.get("relposnedsettings_d")["source_s"] - != data.identity + != "UBX NAV-HEADING" ): return @@ -604,7 +607,7 @@ def _process_NAV_DAHEADING(self, data: UBXMessage): data.accHeading, ) - self.__app.gnss_status.rel_pos_source = data.identity + self.__app.gnss_status.rel_pos_source = "UBX NAV-HEADING" self.__app.gnss_status.rel_pos_heading = relPosHeading self.__app.gnss_status.rel_pos_length = relPosLength self.__app.gnss_status.acc_heading = accHeading diff --git a/src/pygpsclient/uni_handler.py b/src/pygpsclient/uni_handler.py index 9afe5d35..09911b62 100644 --- a/src/pygpsclient/uni_handler.py +++ b/src/pygpsclient/uni_handler.py @@ -3,10 +3,11 @@ Unicore GNSS Protocol handler - handles all incoming UNI messages -Parses individual UNI (Unicore UM98n GNSS) messages (using pyunignss library) -and adds selected attribute values to the app.gnss_status -data dictionary. This dictionary is then used to periodically -update the various user-selectable widget panels. +Parses individual UNI (proprietary Unicore binary) messages +(using pyunignss library) and adds selected attribute values +to the app.gnss_status data dictionary. This dictionary is +then used to periodically update the various user-selectable +widget panels. Created on 27 Jan 2026 @@ -72,35 +73,38 @@ def process_data(self, raw_data: bytes, parsed_data: UNIMessage): :param UNIMessage parsed_data: parsed data """ - if raw_data is None: - return - if self.__app.gnss_status.version_data["hwversion"] == NA: - self.__app.gnss_status.version_data["hwversion"] = "Unicore" - self.__app.set_device_label( - self.__app.gnss_status.version_data["hwversion"] - ) - # self.logger.debug(f"data received {parsed_data.identity}") - self._process_utc(parsed_data) - if parsed_data.identity in ("BESTNAV", "BESTNAVH"): - self._process_BESTNAV(parsed_data) - elif parsed_data.identity in ("PVTSLN",): - self._process_PVTSLN(parsed_data) - elif parsed_data.identity in ( - "ADRNAV", - "ADRNAVH", - "PPPNAV", - "SPPNAV", - "SPPNAVH", - ): - self._process_ADRNAV(parsed_data) - elif parsed_data.identity in ("SATSINFO",): - self._process_SATSINFO(parsed_data) - elif parsed_data.identity in ("SATELLITE",): - self._process_SATELLITE(parsed_data) - elif parsed_data.identity in ("STADOP", "ADRDOP", "PPPDOP"): - self._process_STADOP(parsed_data) - elif parsed_data.identity == "VERSION": - self._process_VERSION(parsed_data) + try: + if raw_data is None: + return + if self.__app.gnss_status.version_data["hwversion"] == NA: + self.__app.gnss_status.version_data["hwversion"] = "Unicore" + self.__app.set_device_label( + self.__app.gnss_status.version_data["hwversion"] + ) + # self.logger.debug(f"data received {parsed_data.identity}") + self._process_utc(parsed_data) + if parsed_data.identity in ("BESTNAV", "BESTNAVH"): + self._process_BESTNAV(parsed_data) + elif parsed_data.identity in ("PVTSLN",): + self._process_PVTSLN(parsed_data) + elif parsed_data.identity in ( + "ADRNAV", + "ADRNAVH", + "PPPNAV", + "SPPNAV", + "SPPNAVH", + ): + self._process_ADRNAV(parsed_data) + elif parsed_data.identity in ("SATSINFO",): + self._process_SATSINFO(parsed_data) + elif parsed_data.identity in ("SATELLITE",): + self._process_SATELLITE(parsed_data) + elif parsed_data.identity in ("STADOP", "ADRDOP", "PPPDOP"): + self._process_STADOP(parsed_data) + elif parsed_data.identity == "VERSION": + self._process_VERSION(parsed_data) + except Exception as err: # pylint: disable=broad-exception-caught + self.logger.exception(err) def _process_utc(self, data: UNIMessage): """ diff --git a/tests/test_static.py b/tests/test_static.py index 50ca86a7..7844f572 100644 --- a/tests/test_static.py +++ b/tests/test_static.py @@ -30,7 +30,6 @@ ) from pygpsclient.helpers import ( area_in_bounds, - bitsval, bytes2unit, col2contrast, dop2str, @@ -59,6 +58,7 @@ point_in_bounds, publicip, reorder_range, + sbfsvid2svid, secs2unit, snr2col, str2rgb, @@ -292,14 +292,6 @@ def teststringvar2val(self): res = stringvar2val(val, att) self.assertEqual(ress[i], res) - def testbitsval(self): - bits = [(7, 1), (8, 8), (22, 2), (24, 4), (40, 16)] - EXPECTED_RESULT = [1, 8, 3, 15, None] - - bm = b"\x01\x08\x03\xf0\xff" - for i, (ps, ln) in enumerate(bits): - res = bitsval(bm, ps, ln) - self.assertEqual(res, EXPECTED_RESULT[i]) def testmapqcompress(self): PREC = 6 @@ -790,6 +782,21 @@ def testtrackbounds(self): self.assertAlmostEqual(center.lat, 53.367617, 7) self.assertAlmostEqual(center.lon, -1.815437, 7) + def testsbfsvid2svid(self): + + res = sbfsvid2svid(218) + self.assertEqual(res, (7, 10)) + res = sbfsvid2svid(64) + self.assertEqual(res, (6, 26)) + res = sbfsvid2svid(150) + self.assertEqual(res, (3, 10)) + res = sbfsvid2svid(2) + self.assertEqual(res, (0, 2)) + res = sbfsvid2svid(199) + self.assertEqual(res, (1, 42)) + res = sbfsvid2svid(223) + self.assertEqual(res, (3, 41)) + def testunusedsats(self): gsv_data = { (1, 1): (0, 0, 0, 0, 5, 0),