diff --git a/.github/workflows/checkpr.yml b/.github/workflows/checkpr.yml index f85678ac..0117a420 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@v6 + - uses: actions/checkout@v7 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: ${{ matrix.python-version }} - name: Install deploy dependencies diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index b3bf21ed..a8b72db8 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -11,9 +11,9 @@ jobs: release-build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Set up Python - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: 3.13 @@ -23,7 +23,7 @@ jobs: python -m build - name: Upload distributions # upload build artifacts - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: release-dists path: dist/ diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 7802bf84..13bbeae5 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@v6 + - uses: actions/checkout@v7 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: ${{ matrix.python-version }} - name: Install deploy dependencies diff --git a/.vscode/settings.json b/.vscode/settings.json index c7d2f360..58b12cc0 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -25,5 +25,12 @@ "python.systemPath": "/usr/local/bin/python3.14", "python.defaultInterpreterPath": "${userHome}/pygpsclient/bin/python3", //"python.defaultInterpreterPath": "C:/Users/steve/pygpsclient/Scripts/python.exe", - "python.testing.unittestEnabled": false + "python.testing.unittestEnabled": false, + "python-envs.pythonProjects": [ + { + "path": ".", + "envManager": "ms-python.python:venv", + "packageManager": "ms-python.python:pip" + } + ] } \ No newline at end of file diff --git a/CITATION.bib b/CITATION.bib deleted file mode 100644 index 67c454f0..00000000 --- a/CITATION.bib +++ /dev/null @@ -1,8 +0,0 @@ -@Misc{PyGPSClient, - author = {{SEMU Consulting}}, - howpublished = {GitHub repository}, - note = {Viewed last: xxxx:xx:xx}, - title = {Python Graphical GPS Client Application supporting NMEA, UBX, SBF, UNI, QGC, RTCM3, NTRIP & SPARTN Protocols}, - year = {2022}, - url = {https://github.com/semuconsulting/pygpsclient}, -} diff --git a/CITATION.cff b/CITATION.cff new file mode 100644 index 00000000..b366c7d2 --- /dev/null +++ b/CITATION.cff @@ -0,0 +1,13 @@ +cff-version: 1.2.0 +message: "If you use this software, please cite it using the metadata from this file." +title: "PyGPSClient" +abstract: "Python GNSS Testing, Diagnostic and Configuration GUI Utility." +authors: + - family-names: "Smith" + given-names: "Steve" + orcid: "https://orcid.org/0009-0002-5751-8719" + - name: "semuadmin" +version: 1.7.6 +date-released: 2020-10-04 +license: "BSD-3-Clause" +repository-code: "https://github.com/semuconsulting/PyGPSClient" diff --git a/README.md b/README.md index d042a691..69e0e259 100644 --- a/README.md +++ b/README.md @@ -209,7 +209,7 @@ For more comprehensive installation instructions, please refer to [INSTALLATION. |![sysmon widget](https://github.com/semuconsulting/PyGPSClient/blob/master/images/sysmon_widget.png?raw=true)| System Monitor widget showing device cpu, memory and I/O utilisation (*GNSS receiver must be capable of outputting UBX MON-SYS/MON-COMMS or SBF ReceiverStatus messages*). Tick checkbox to toggle between actual (cumulative) I/O stats and pending I/O. Primarily intended for u-blox modules, but can display limited system information for other devices. | |![scatterplot widget](https://github.com/semuconsulting/PyGPSClient/blob/master/images/scatterplot_widget.png?raw=true)| Scatterplot widget showing variability in position reporting over time. (Optional) Enter fixed reference position. Select Average to center plot on dynamic average position (*displayed at top left*), or Fixed to center on fixed reference position (*if entered*). Check Autorange to set plot range automatically. Set the update interval (e.g. 4 = every 4th navigation solution). Use the range slider or mouse wheel to adjust plot range. Right-click to set fixed reference point to the current mouse cursor position. Double-click to clear the existing data. | |![rover widget](https://github.com/semuconsulting/PyGPSClient/blob/master/images/rover_widget.png?raw=true) | Relative Position (formerly 'Rover') widget plots relative 2D bearing, distance, track and status information for the following configurations: (i) NAV-RELPOSNED - position of roving receiver relative to a fixed or moving RTK base (including an NTRIP mountpoint); (ii) RTCM 1005/6 - position of roving receiver relative to a fixed RTK NTRIP antenna reference point (ARP); (iii) NAV-DAHEADING - static heading ('yaw') from a dual-antenna receiver e.g. u-blox ZED-X20D. Double-click to clear existing plot. | -|![chart view](https://github.com/semuconsulting/PyGPSClient/blob/master/images/chart_widget.png?raw=true) | Chart widget acts as a multi-channel "plotter", allowing the user to plot a series of named numeric data attributes from any parsed GNSS data source, with configurable y (value) and x (time) axes. By default, the number of channels is set to 4, but this can be manually edited by the user via the json configuration file setting `chartsettings_d["numchn_n"]`. For each channel, user can select: (*optional*) identity of message source e.g. `NAV-PVT`; attribute name e.g. `hAcc`; scaling factor (divisor) e.g. 1000; y axis range e.g. 0 - 5. Wildcards are available for attribute groups - "\*" (average of group values), "+" (maximum of group values), "-" (minimum of group values) e.g. `cno*` will plot the average `cno` value for a group of satellites. Double-click to clear the existing data. Double-right-click to save the current chart data to the clipboard in CSV format, which can be directly pasted into a spreadsheet application. | +|![chart view](https://github.com/semuconsulting/PyGPSClient/blob/master/images/chart_widget.png?raw=true) | Chart widget acts as a multi-channel "plotter", allowing the user to plot a series of named numeric data attributes from any parsed GNSS data source, with configurable y (value) and x (time) axes. By default, the number of channels is set to 4, but this can be manually edited by the user via the json configuration file setting `chartsettings_d["numchn_n"]` or Menu..Options..App Configuration dialog (*save configuration and restart after any changes*). For each channel, user can select: (*optional*) identity of message source e.g. `NAV-PVT`; attribute name e.g. `hAcc`; scaling factor (divisor) e.g. 1000; y axis range e.g. 0 - 5. Wildcards are available for attribute groups - "\*" (average of group values), "+" (maximum of group values), "-" (minimum of group values) e.g. `cno*` will plot the average `cno` value for a group of satellites. Double-click to clear the existing data. Double-right-click to save the current chart data to the clipboard in CSV format, which can be directly pasted into a spreadsheet application. | |![attitude widget](https://github.com/semuconsulting/PyGPSClient/blob/master/images/attitude_widget.png?raw=true) | Attitude Monitor widget (*formerly "IMU Monitor"*) showing current orientation/attitude (roll, pitch, yaw *aka 'static heading'*) and status from a variety of IMU, Dead Reckoning, Dual Antenna or other 2D/3D attitude message sources. Select range in degrees (from ±1 to ±180 degrees). | --- @@ -537,6 +537,8 @@ For further details, refer to the `pygnssutils` homepage at [https://github.com/ Copyright © 2020-2026, semuadmin (Steve Smith) +[![ORCID iD icon](https://orcid.org/sites/default/files/images/orcid_16x16.png) https://orcid.org/0009-0002-5751-8719](https://orcid.org/0009-0002-5751-8719) + Application icons from [iconmonstr](https://iconmonstr.com/license/) ©. --- diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 53e87ad5..4d5de6dd 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,5 +1,12 @@ # PyGPSClient Release Notes +### RELEASE 1.7.6 + +1. Widget performance improvements. +1. Add Python 3.15 classifier to pyproject.toml. +1. Update github actions to latest versions. +1. Add Chart plotter channels to App Configuration dialog. + ### RELEASE 1.7.5 FIXES: diff --git a/pyproject.toml b/pyproject.toml index 0fdd2087..b0420131 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,26 +5,36 @@ build-backend = "setuptools.build_meta:__legacy__" [project] name = "pygpsclient" dynamic = ["version"] -authors = [{ name = "Steve Smith", email = "semuadmin@semuconsulting.com" }] -maintainers = [{ name = "Steve Smith", email = "semuadmin@semuconsulting.com" }] -description = "GNSS Diagnostic and UBX Configuration GUI Application" +authors = [{ name = "Steve Smith" }, { email = "semuadmin@semuconsulting.com" }] +maintainers = [ + { name = "Steve Smith" }, + { email = "semuadmin@semuconsulting.com" }, +] +description = "GNSS Receiver Testing, Diagnostic and Configuration GUI Application" license = "BSD-3-Clause" license-files = ["LICENSE"] keywords = [ - "PyGPSClient", - "GNSS", - "GPS", + "BEIDOU", + "DGPS", "GALILEO", + "GIS", "GLONASS", - "BEIDOU", + "GNSS", + "GPS", + "NAVIC", "NMEA", - "UBX", - "RTCM", "NTRIP", - "SPARTN", + "PyGPSClient", + "Quectel", + "QZSS", + "RTCM", "RTK", - "DGPS", + "Septentrio", + "SPARTN", + "u-blox", "u-center", + "UBX", + "Unicore", ] readme = "README.md" requires-python = ">=3.10" @@ -44,19 +54,13 @@ classifiers = [ "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", + "Programming Language :: Python :: 3.15", "Topic :: Utilities", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: Scientific/Engineering :: GIS", ] -dependencies = [ - "requests>=2.34.0", - "Pillow>=12.0.0", - "pygnssutils>=1.2.7", - "pyunigps>=1.0.0", - "pynmeagps>=1.1.7", - "pyubx2>=1.3.5", -] +dependencies = ["requests>=2.34.0", "Pillow>=12.0.0", "pygnssutils>=1.2.7"] [project.scripts] pygpsclient = "pygpsclient.__main__:main" diff --git a/src/pygpsclient/_version.py b/src/pygpsclient/_version.py index f3d54cd4..53f0cbc2 100644 --- a/src/pygpsclient/_version.py +++ b/src/pygpsclient/_version.py @@ -8,4 +8,4 @@ :license: BSD 3-Clause """ -__version__ = "1.7.5" +__version__ = "1.7.6" diff --git a/src/pygpsclient/app_config_dialog.py b/src/pygpsclient/app_config_dialog.py index 8d72ea14..3e8f485c 100644 --- a/src/pygpsclient/app_config_dialog.py +++ b/src/pygpsclient/app_config_dialog.py @@ -63,6 +63,7 @@ ) MAP_INTERVALS = ("1", "2", "5", "10", "30", "60", "120", "240", "360") LOG_SIZES = ("1", "2", "5", "10", "20", "50", "100", "200", "500", "1000") +PLOT_CHANS = [str(i) for i in range(4, 21, 2)] class AppConfigDialog(ToplevelDialog): @@ -89,8 +90,10 @@ def __init__(self, app: Tk, *args, **kwargs): self._maprefresh = StringVar() self._mapkey = StringVar() self._logsize = StringVar() + self._plotchans = StringVar() self._resizedialog = BooleanVar() self._transientdialog = BooleanVar() + self._showunused = BooleanVar() self._body() self._do_layout() @@ -149,6 +152,17 @@ def _body(self): wrap=True, textvariable=self._logsize, ) + self._lbl_plotchans = Label( + self._frm_body, text="Chart Plotter Channels", anchor=W + ) + self._spn_plotchans = Spinbox( + self._frm_body, + values=PLOT_CHANS, + width=6, + state=READONLY, + wrap=True, + textvariable=self._plotchans, + ) self._lbl_resizedialog = Label( self._frm_body, text="Resizeable TopLevel Dialogs", anchor=W ) @@ -165,6 +179,14 @@ def _body(self): text="", variable=self._transientdialog, ) + self._lbl_showunused = Label( + self._frm_body, text="Show Sats With Null C/No", anchor=W + ) + self._chk_showunused = Checkbutton( + self._frm_body, + text="", + variable=self._showunused, + ) self._btn_save = Button( self._frm_body, command=self._on_save_updates, @@ -181,21 +203,25 @@ def _do_layout(self): self._lbl_guirefresh.grid(column=0, row=0, sticky=W) self._spn_guirefresh.grid(column=1, row=0, sticky=W) self._lbl_guirefreshu.grid(column=2, row=0, padx=2, sticky=W) - self._lbl_maprefresh.grid(column=0, row=1, sticky=W) - self._spn_maprefresh.grid(column=1, row=1, sticky=W) - self._lbl_maprefreshu.grid(column=2, row=1, padx=2, sticky=W) - self._lbl_logsize.grid(column=0, row=2, sticky=W) - self._spn_logsize.grid(column=1, row=2, sticky=W) - self._lbl_logsizeu.grid(column=2, row=2, padx=2, sticky=W) - self._frm_mapkey.grid(column=0, row=3, sticky=EW) + self._lbl_logsize.grid(column=0, row=1, sticky=W) + self._spn_logsize.grid(column=1, row=1, sticky=W) + self._lbl_logsizeu.grid(column=2, row=1, padx=2, sticky=W) + self._lbl_plotchans.grid(column=0, row=2, sticky=W) + self._spn_plotchans.grid(column=1, row=2, sticky=W) + self._lbl_maprefresh.grid(column=0, row=3, sticky=W) + self._spn_maprefresh.grid(column=1, row=3, sticky=W) + self._lbl_maprefreshu.grid(column=2, row=3, padx=2, sticky=W) + self._frm_mapkey.grid(column=0, row=4, sticky=EW) self._lbl_mapkey.grid(column=0, row=0, sticky=W) self._btn_mapkey.grid(column=1, row=0, sticky=E) - self._ent_mapkey.grid(column=1, row=3, columnspan=2, sticky=EW) - self._lbl_resizedialog.grid(column=0, row=4, sticky=W) - self._chk_resizedialog.grid(column=1, row=4, columnspan=2, sticky=W) - self._lbl_transientdialog.grid(column=0, row=5, sticky=W) - self._chk_transientdialog.grid(column=1, row=5, columnspan=2, sticky=W) - self._btn_save.grid(column=1, row=6, columnspan=2, padx=4, pady=3, sticky=E) + self._ent_mapkey.grid(column=1, row=4, columnspan=2, sticky=EW) + self._lbl_resizedialog.grid(column=0, row=5, sticky=W) + self._chk_resizedialog.grid(column=1, row=5, columnspan=2, sticky=W) + self._lbl_transientdialog.grid(column=0, row=6, sticky=W) + self._chk_transientdialog.grid(column=1, row=6, columnspan=2, sticky=W) + self._lbl_showunused.grid(column=0, row=7, sticky=W) + self._chk_showunused.grid(column=1, row=7, columnspan=2, sticky=W) + self._btn_save.grid(column=1, row=8, columnspan=2, padx=4, pady=3, sticky=E) self._frm_body.columnconfigure(2, weight=1) def _attach_events(self): @@ -208,8 +234,10 @@ def _attach_events(self): self._guirefresh, self._maprefresh, self._logsize, + self._plotchans, self._resizedialog, self._transientdialog, + self._showunused, self._mapkey, ): var.trace_update(TRACEMODE_WRITE, self._on_update, True) @@ -224,11 +252,15 @@ def _reset(self): ) self._maprefresh.set(self.__app.configuration.get("mapupdateinterval_n")) self._logsize.set(int(self.__app.configuration.get("logsize_n") / 1048576)) + self._plotchans.set( + int(self.__app.configuration.get("chartsettings_d")["numchn_n"]) + ) self._mapkey.set(self.__app.configuration.get("mqapikey_s")) self._resizedialog.set(int(self.__app.configuration.get("resizeable_dialog_b"))) self._transientdialog.set( int(self.__app.configuration.get("transient_dialog_b")) ) + self._showunused.set(int(self.__app.configuration.get("unusedsat_b"))) self._enable_save_button(False) self.set_status_label("Update with caution! Restart after updating", ERRCOL) @@ -250,6 +282,10 @@ def _on_update(self, var, index, mode): self.__app.configuration.get("logsize_n") != int(self._logsize.get()) * 1048576 ) + | ( + self.__app.configuration.get("chartsettings_d")["numchn_n"] + != int(self._plotchans.get()) + ) | (self.__app.configuration.get("mqapikey_s") != self._mapkey.get()) | ( self.__app.configuration.get("resizeable_dialog_b") @@ -259,6 +295,10 @@ def _on_update(self, var, index, mode): self.__app.configuration.get("transient_dialog_b") != int(self._transientdialog.get()) ) + | ( + self.__app.configuration.get("unusedsat_b") + != int(self._showunused.get()) + ) ) msg = "Settings updated" if updates else "" if int(self._maprefresh.get()) < 60: @@ -295,6 +335,15 @@ def _on_save_updates(self): ) self.__app.configuration.set("mapupdateinterval_n", int(self._maprefresh.get())) self.__app.configuration.set("logsize_n", int(self._logsize.get()) * 1048576) + chartsettings = self.__app.configuration.get("chartsettings_d") + self.__app.configuration.set( + "chartsettings_d", + { + "numchn_n": int(self._plotchans.get()), + "timrng_n": chartsettings["timrng_n"], + "maxpoints_n": chartsettings["maxpoints_n"], + }, + ) self.__app.configuration.set("mqapikey_s", self._mapkey.get()) self.__app.configuration.set( "resizeable_dialog_b", int(self._resizedialog.get()) @@ -302,6 +351,7 @@ def _on_save_updates(self): self.__app.configuration.set( "transient_dialog_b", int(self._transientdialog.get()) ) + self.__app.configuration.set("unusedsat_b", int(self._showunused.get())) self.set_status_label("Save configuration to file and restart", OKCOL) if self.__app.save_config() != "": self.set_status_label("Save cancelled", INFOCOL) diff --git a/src/pygpsclient/attitude_frame.py b/src/pygpsclient/attitude_frame.py index 101b286d..d94e1dab 100644 --- a/src/pygpsclient/attitude_frame.py +++ b/src/pygpsclient/attitude_frame.py @@ -13,7 +13,6 @@ :license: BSD 3-Clause """ -import logging from tkinter import ( EW, NSEW, @@ -85,7 +84,6 @@ def __init__(self, app: Tk, parent: Frame, *args, **kwargs): :param kwargs: Optional kwargs to pass to Frame parent class """ self.__app = app - self.logger = logging.getLogger(__name__) super().__init__(parent, *args, **kwargs) @@ -352,18 +350,9 @@ def update_frame(self): tag=DATA, ) - self.update_idletasks() - except (KeyError, ValueError): self._canvas.delete(DATA) - # MEMORY LEAK DEBUG - # tot = len(self._canvas.find_all()) - # tags = {} - # for tag in (TAG_DATA, TAG_WAIT): - # tags[tag] = len(self._canvas.find_withtag(tag)) - # self.logger.debug((tot, tags)) - def _flag_range(self, over: bool = False): """ Flag range spinbox if data is overrange. diff --git a/src/pygpsclient/banner_frame.py b/src/pygpsclient/banner_frame.py index a53399b8..83f965d1 100644 --- a/src/pygpsclient/banner_frame.py +++ b/src/pygpsclient/banner_frame.py @@ -411,7 +411,6 @@ def update_frame(self): self._update_dop(units) self._update_dgps(units) self._update_psstats() - self.update_idletasks() def _update_time(self): """ diff --git a/src/pygpsclient/chart_frame.py b/src/pygpsclient/chart_frame.py index 0bc5969d..a2219373 100644 --- a/src/pygpsclient/chart_frame.py +++ b/src/pygpsclient/chart_frame.py @@ -508,7 +508,6 @@ def update_frame(self): break self._update_plot(self._chart_data) - self.update_idletasks() def init_frame(self): """ @@ -604,7 +603,6 @@ def _update_plot(self, data: dict): chn=chn, tags=(TAG_DATA,), ) - self.update_idletasks() def _on_clipboard(self, event): # pylint: disable=unused-argument """ diff --git a/src/pygpsclient/globals.py b/src/pygpsclient/globals.py index 98c58399..51338939 100644 --- a/src/pygpsclient/globals.py +++ b/src/pygpsclient/globals.py @@ -136,7 +136,6 @@ GNSS_ERR_EVENT = "<>" GNSS_EVENT = "<>" GNSS_TIMEOUT_EVENT = "<>" -GPSEPOCH0 = datetime(1980, 1, 6) # for Wno and Tow calculations GPX_NS = ( 'xmlns="http://www.topografix.com/GPX/1/1" ' 'creator="PyGPSClient" version="1.1" ' diff --git a/src/pygpsclient/helpers.py b/src/pygpsclient/helpers.py index 057a92c5..7d425b7a 100644 --- a/src/pygpsclient/helpers.py +++ b/src/pygpsclient/helpers.py @@ -45,9 +45,8 @@ from PIL import Image, ImageTk from pygnssutils import version as PGVERSION -from pynmeagps import WGS84_SMAJ_AXIS, NMEAMessage, haversine +from pynmeagps import WGS84_SMAJ_AXIS, NMEAMessage from pynmeagps import version as NMEAVERSION -from pynmeagps import wnotow2utc from pyqgc import version as QGCVERSION from pyrtcm import version as RTCMVERSION from pysbf2 import version as SBFVERSION @@ -72,7 +71,6 @@ CLICK_CURSOR, ERRCOL, FIXLOOKUP, - GPSEPOCH0, ICON_EYEOFF, ICON_EYEON, M2FT, @@ -145,7 +143,7 @@ def validate( and error highlighting. :param Entry self: tkinter entry widget instance - :param int valmode: int representing validation type - can be OR'd + :param int valmode: int representing validation type :param int | float low: optional min value, or length of field in \ bits for VALSIGNED/VALUNSIGNED :param int | float high: optional max value @@ -427,51 +425,7 @@ def col2contrast(col: str) -> str: r, g, b = str2rgb(col) luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255 - return "black" if luminance > 0.5 else "white" - - -def corrage2int(code: int) -> int: - """ - Convert NAV-PVT lastCorrectionAge value to age in seconds. - - :param int code: diff age code from NAV-PVT - :return: string indicating diff age in seconds - :rtype: int - """ - - lookup = { - 0: 0, - 1: 1, - 2: 2, - 3: 5, - 4: 10, - 5: 15, - 6: 20, - 7: 30, - 8: 45, - 9: 60, - 10: 90, - 11: 120, - } - - return lookup.get(code, 0) - - -def date2wnotow(dat: datetime) -> tuple: - """ - Get GPS Week number (Wno) and Time of Week (Tow) - for given datetime. - - GPS Epoch 0 = 6th Jan 1980 - - :param datetime dat: calendar date - :return: tuple of (Wno, Tow) - :rtype: tuple - """ - - wno = int((dat - GPSEPOCH0).days / 7) - tow = ((dat.weekday() + 1) % 7) * 86400 - return wno, tow + return "#000000" if luminance > 0.5 else "#ffffff" def dop2str(dop: float) -> str: @@ -583,33 +537,6 @@ def fontdim(fmt: str, fnt: font.Font, angle: int = 0) -> tuple[float, float]: return rw, rh -def get_mp_distance(lat: float, lon: float, mp: list) -> float: - """ - Get distance to mountpoint from current location (if known). - - The sourcetable mountpoint entry is a list where index [0] - is the name and indices [8] & [9] are the lat/lon. Not all - sourcetable entries provide this information. - - :param float lat: current latitude - :param float lon: current longitude - :param list mp: sourcetable mountpoint entry - :return: distance to mountpoint in km, or None if n/a - :rtype: float or None - """ - - dist = None - try: - if len(mp) > 9: # if location provided for this mountpoint - lat2 = float(mp[8]) - lon2 = float(mp[9]) - dist = haversine(lat, lon, lat2, lon2) - except (ValueError, TypeError): - pass - - return dist - - def get_mp_info(srt: list) -> dict: """ Get mountpoint information from sourcetable entry. @@ -1063,32 +990,6 @@ def normalise_area(points: tuple) -> Area: return Area(minlat, minlon, maxlat, maxlon) -def parse_rxmspartnkey(msg: UBXMessage) -> list: - """ - Extract dates and keys from RXM-SPARTNKEY message. - - :param UBXMessage msg: RXM-SPARTNKEY message - :return: list of (key, valid from date) tuples - :rtype: list - """ - - keys = [] - pos = 0 - for i in range(msg.numKeys): - lkey = getattr(msg, f"keyLengthBytes_{i+1:02}") - wno = getattr(msg, f"validFromWno_{i+1:02}") - tow = getattr(msg, f"validFromTow_{i+1:02}") - dat = wnotow2utc(wno, int(tow * 1000), None, "G", True) - key = "" - for n in range(0 + pos, lkey + pos): - keyb = getattr(msg, f"key_{n+1:02}") - key += f"{keyb:02x}" - keys.append((key, dat)) - pos += lkey - - return keys - - def point_in_bounds(bounds: Area, point: Point) -> bool: """ Check if given point is within canvas bounding box. @@ -1110,42 +1011,6 @@ def point_in_bounds(bounds: Area, point: Point) -> bool: ) -def pos2iso6709(lat: float, lon: float, alt: float, crs: str = "WGS_84") -> str: - """ - convert decimal degrees and alt to iso6709 format. - - :param float lat: latitude - :param float lon: longitude - :param float alt: altitude - :param float crs: coordinate reference system (default = WGS_84) - :return: position in iso6709 format - :rtype: str - - """ - - if not ( - isinstance(lat, (float, int)) - and isinstance(lon, (float, int)) - and isinstance(alt, (float, int)) - ): - return "" - lati = "-" if lat < 0 else "+" - loni = "-" if lon < 0 else "+" - alti = "-" if alt < 0 else "+" - iso6709 = ( - lati - + str(abs(lat)) - + loni - + str(abs(lon)) - + alti - + str(alt) - + "CRS" - + crs - + "/" - ) - return iso6709 - - def publicip() -> str: """ Get public IP address from ipinfo REST API. diff --git a/src/pygpsclient/levelsview_frame.py b/src/pygpsclient/levelsview_frame.py index 63a7a7b6..f166eddc 100644 --- a/src/pygpsclient/levelsview_frame.py +++ b/src/pygpsclient/levelsview_frame.py @@ -201,7 +201,6 @@ def update_frame(self): tags=TAG_DATA, ) offset += colwidth - self.update_idletasks() def _on_resize(self, event): # pylint: disable=unused-argument """ diff --git a/src/pygpsclient/rover_frame.py b/src/pygpsclient/rover_frame.py index aba5763b..e9e913b7 100644 --- a/src/pygpsclient/rover_frame.py +++ b/src/pygpsclient/rover_frame.py @@ -270,7 +270,6 @@ def update_frame(self): tags=TAG_DATA, ) self._canvas.create_circle(x, y, 3, fill=PNTCOL, outline=PNTCOL, tags=TAG_DATA) - self.update_idletasks() def _set_range(self, distance: float): """ diff --git a/src/pygpsclient/scatter_frame.py b/src/pygpsclient/scatter_frame.py index 5aaff3d1..ef74ba13 100644 --- a/src/pygpsclient/scatter_frame.py +++ b/src/pygpsclient/scatter_frame.py @@ -395,11 +395,11 @@ def _draw_stats(self, lbl_font: object): 5, y, text=std, fill=PNTCOL, font=lbl_font, anchor=NW, tags=TAG_DATA ) y += fh - np = len(self._points) - pts = f"Pts: {np} {'!' if np >= self._maxpoints else ''}" - self._canvas.create_text( - 5, y, text=pts, fill=PNTCOL, font=lbl_font, anchor=NW, tags=TAG_DATA - ) + # np = len(self._points) + # pts = f"Pts: {np} {'!' if np >= self._maxpoints else ''}" + # self._canvas.create_text( + # 5, y, text=pts, fill=PNTCOL, font=lbl_font, anchor=NW, tags=TAG_DATA + # ) def _draw_point(self, position: Point, color: str = PNTCOL, size: int = 2): """ @@ -488,14 +488,13 @@ def _update_plot(self): return lp = len(self._points) - 1 + col = PNTCOL for i, pnt in enumerate(self._points): if i == lp: - break - self._draw_point(pnt, PNTCOL) - self.update_idletasks() + col = PNTTOPCOL + self._draw_point(pnt, col) if self._fixed is not None: self._draw_point(self._fixed, FIXCOL, 3) - self._draw_point(self._points[-1], PNTTOPCOL) self._draw_stats(self._canvas.font) diff --git a/src/pygpsclient/signalsview_frame.py b/src/pygpsclient/signalsview_frame.py index 8425a958..ef4aef0c 100644 --- a/src/pygpsclient/signalsview_frame.py +++ b/src/pygpsclient/signalsview_frame.py @@ -343,7 +343,6 @@ def update_frame(self): tags=TAG_DATA, ) offset += colwidth - self.update_idletasks() def _on_resize(self, event): # pylint: disable=unused-argument """ diff --git a/src/pygpsclient/skyview_frame.py b/src/pygpsclient/skyview_frame.py index 03a0b92f..10c48dca 100644 --- a/src/pygpsclient/skyview_frame.py +++ b/src/pygpsclient/skyview_frame.py @@ -144,7 +144,6 @@ def update_frame(self): font=self._canvas.font, tags=TAG_DATA, ) - self.update_idletasks() except ValueError: pass diff --git a/src/pygpsclient/spectrum_frame.py b/src/pygpsclient/spectrum_frame.py index 4ee39b3f..388f48aa 100644 --- a/src/pygpsclient/spectrum_frame.py +++ b/src/pygpsclient/spectrum_frame.py @@ -331,7 +331,6 @@ def _update_plot( width=OL_WID, tags=(mode, TAG_DATA), ) - self.update_idletasks() # display any marked db/hz coordinate if self._chartpos is not None and self._redraw: diff --git a/src/pygpsclient/sqlite_handler.py b/src/pygpsclient/sqlite_handler.py index a36cc8c0..16c87413 100644 --- a/src/pygpsclient/sqlite_handler.py +++ b/src/pygpsclient/sqlite_handler.py @@ -187,15 +187,15 @@ def open( return SQLOK except AttributeError as err: self.__app.set_status_label(DLGDBSQLERR.format(err), errcol) - self.logger.debug(traceback.format_exc()) + # self.logger.debug(traceback.format_exc()) return NOEXT # extensions not supported except sqlite3.OperationalError as err: self.__app.set_status_label(DLGDBSQLERR.format(err), errcol) - self.logger.debug(traceback.format_exc()) + # self.logger.debug(traceback.format_exc()) return NOMODS # no mod_spatial extension found except sqlite3.Error as err: self.__app.set_status_label(DLGDBSQLERR.format(err), errcol) - self.logger.debug(traceback.format_exc()) + # self.logger.debug(traceback.format_exc()) return SQLERR # other sqlite error def close(self): diff --git a/src/pygpsclient/stream_handler.py b/src/pygpsclient/stream_handler.py index 5257a42a..e2d5761f 100644 --- a/src/pygpsclient/stream_handler.py +++ b/src/pygpsclient/stream_handler.py @@ -89,7 +89,7 @@ UBXSIMULATOR, ) from pygpsclient.strings import WARNING -from pygpsclient.widget_state import FRAME, VISIBLE, WDGCHART, WDGCONSOLE +from pygpsclient.widget_state import VISIBLE, WDGCHART, WDGCONSOLE class StreamHandler: diff --git a/src/pygpsclient/sysmon_frame.py b/src/pygpsclient/sysmon_frame.py index 55ac6d71..69343eae 100644 --- a/src/pygpsclient/sysmon_frame.py +++ b/src/pygpsclient/sysmon_frame.py @@ -256,8 +256,6 @@ def update_frame(self): font=self._font, tags=TAG_DATA, ) - - self.update_idletasks() except KeyError: # invalid sysmon-data or comms-data self.init_chart() diff --git a/src/pygpsclient/ubx_handler.py b/src/pygpsclient/ubx_handler.py index c709fee8..ea51c33f 100644 --- a/src/pygpsclient/ubx_handler.py +++ b/src/pygpsclient/ubx_handler.py @@ -18,10 +18,10 @@ import logging from time import time -from pyubx2 import UBXMessage, itow2utc +from pyubx2 import LASTCORRECTIONAGE, UBXMessage, itow2utc from pygpsclient.globals import BSR, GLONASS_NMEA, UTF8 -from pygpsclient.helpers import corrage2int, fix2desc, hdg2yaw, ned2vector, svid2gnssid +from pygpsclient.helpers import fix2desc, hdg2yaw, ned2vector, svid2gnssid from pygpsclient.strings import DLGTSERVER, DLGTUBX, DLGTUBXLEGACY, NA from pygpsclient.widget_state import VISIBLE, WDGSIGNALS, WDGSPECTRUM, WDGSYSMON @@ -296,7 +296,9 @@ def _process_NAV_PVT(self, data: UBXMessage): elif hasattr(data, "diffSoln"): self.__app.gnss_status.diff_corr = data.diffSoln if data.lastCorrectionAge != 0: - self.__app.gnss_status.diff_age = corrage2int(data.lastCorrectionAge) + self.__app.gnss_status.diff_age = LASTCORRECTIONAGE.get( + data.lastCorrectionAge, 0 + ) ims = self.__app.gnss_status.imu_data if data.headVehValid == 1: diff --git a/tests/test_static.py b/tests/test_static.py index 3482a497..f981d363 100644 --- a/tests/test_static.py +++ b/tests/test_static.py @@ -13,16 +13,14 @@ # pylint: disable=missing-docstring import unittest -from datetime import datetime, timezone from pynmeagps import SET, NMEAMessage -from pyubx2 import UBXMessage, UBXReader +from pyubx2 import UBXMessage from pygpsclient.configuration import Configuration, INITMARKER from pygpsclient.gnss_status import GNSSStatus from pygpsclient.globals import ( Area, - AreaXY, Point, TrackPoint, UI, @@ -35,18 +33,14 @@ bitsval, bytes2unit, col2contrast, - corrage2int, - date2wnotow, dop2str, fix2desc, ft2m, - get_mp_distance, get_mp_info, get_range, get_units, get_point_at_vector, get_track_bounds, - haversine, hdg2yaw, hsv2rgb, isot2dt, @@ -62,9 +56,7 @@ ned2vector, nmea2preset, normalise_area, - parse_rxmspartnkey, point_in_bounds, - pos2iso6709, publicip, reorder_range, secs2unit, @@ -236,12 +228,6 @@ def testknots2ms(self): res = knots2ms("xxx") self.assertEqual(res, 0) - def testpos2iso6709(self): - res = pos2iso6709(53.12, -2.165, 35) - self.assertEqual(res, "+53.12-2.165+35CRSWGS_84/") - res = pos2iso6709("", -2.165, 35) - self.assertEqual(res, "") - def testhsv2rgb(self): res = hsv2rgb(0.5, 0.2, 0.9) self.assertEqual(res, "#b7e5e5") @@ -281,9 +267,9 @@ def testsvid2gnss(self): def testcol2contrast(self): res = col2contrast("#ff0000") - self.assertEqual(res, "white") + self.assertEqual(res, "#ffffff") res = col2contrast("#dddddd") - self.assertEqual(res, "black") + self.assertEqual(res, "#000000") def testfix2desc(self): EXPECTED_RESULT = ["3D", "RTK FIXED", "RTK FLOAT", "3D", "NO FIX"] @@ -293,65 +279,6 @@ def testfix2desc(self): res = fix2desc(fix, msg) self.assertEqual(res, EXPECTED_RESULT[i]) - def testcorrage2int(self): - EXPECTED_RESULT = [0, 5, 20, 60, 120, 0] - fixes = (0, 3, 6, 9, 11, 15) - for i, fix in enumerate(fixes): - res = corrage2int(fix) - self.assertEqual(res, EXPECTED_RESULT[i]) - - def testhaversine(self): - res = haversine(51.23, -2.41, 34.205, 56.34) - self.assertAlmostEqual(res, 5010.721853179245, 4) - res = haversine(-12.645, 34.867, 145.1745, -56.27846) - self.assertAlmostEqual(res, 10715.370876703888, 4) - - def testgetmpdistance(self): - mp = [ - "TKC-EGA", - "Wrens, GA", - "RTCM 3.2", - "1005(1),1074(1),1084(1),1094(1),1124(1),1230(1)", - "", - "GPS+GLO+GAL+BDS", - "SNIP", - "USA", - "33.31", - "-82.44", - "1", - "0", - "sNTRIP", - "none", - "N", - "N", - "4200", - "", - ] - res = get_mp_distance(34.123, 14.6743, mp) - self.assertAlmostEqual(res, 8588.391732771786, 4) - mp = [ - "tobetsu-tsujino", - "Tobetsu", - "RTCM 3.2", - "1005(1),1077(1),1087(1),1127(1),1230(10)", - "", - "GPS+GLO+BDS", - "SNIP", - "JPN", - "43.22", - "141.52", - "1", - "0", - "sNTRIP", - "none", - "N", - "N", - "8300", - "", - ] - res = get_mp_distance(-34.123, -8.6743, mp) - self.assertAlmostEqual(res, 17274.381937035745, 4) - def teststringvar2val(self): vals = [ ("53", "U001"), @@ -365,27 +292,6 @@ def teststringvar2val(self): res = stringvar2val(val, att) self.assertEqual(ress[i], res) - def testdate2wnotow(self): - dats = [ - (2023, 1, 1), - (2005, 11, 5), - (2020, 8, 20), - (2014, 3, 16), - (2023, 5, 21), - (2023, 5, 27), - ] - vals = [ - (2243, 0), - (1347, 518400), - (2119, 345600), - (1784, 0), - (2263, 0), - (2263, 518400), - ] - for i, dat in enumerate(dats): - y, m, d = dat - self.assertEqual(date2wnotow(datetime(y, m, d)), vals[i]) - def testbitsval(self): bits = [(7, 1), (8, 8), (22, 2), (24, 4), (40, 16)] EXPECTED_RESULT = [1, 8, 3, 15, None] @@ -395,17 +301,6 @@ def testbitsval(self): res = bitsval(bm, ps, ln) self.assertEqual(res, EXPECTED_RESULT[i]) - def testparserxm(self): - # EXPECTED_RESULT = [('0c00', datetime(1988, 3, 1, 7, 39, 55, tzinfo=timezone.utc)), ('290900', datetime(1988, 7, 4, 2, 39, 55, tzinfo=timezone.utc))] - EXPECTED_RESULT = [('0c00', datetime(1980, 11, 4, 7, 40, tzinfo=timezone.utc)), ('290900', datetime(1980, 11, 3, 2, 40, tzinfo=timezone.utc))] - RXM_SPARTNKEY = b"\xb5b\x026\x19\x00\x01\x02\x00\x00\x00\x02+\x00\xd0Y\xc8\r\x00\x03+\x00\x00\xdfl\x0e\x0c\x00)\t\x00D;" - msg = UBXReader.parse(RXM_SPARTNKEY) - res = parse_rxmspartnkey(msg) - print(f'"{res}",') - self.assertTrue(len(res) == 2) - self.assertIsInstance(res[0][1], datetime) - self.assertIsInstance(res[1][1], datetime) - def testmapqcompress(self): PREC = 6 points = [