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/README.md b/README.md index d042a691..4a108ec6 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). | --- diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 53e87ad5..fba91ee3 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,5 +1,11 @@ # PyGPSClient Release Notes +### RELEASE 1.7.6 + +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..678cc55d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,8 +5,11 @@ 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" }] +authors = [{ name = "Steve Smith" }, { email = "semuadmin@semuconsulting.com" }] +maintainers = [ + { name = "Steve Smith" }, + { email = "semuadmin@semuconsulting.com" }, +] description = "GNSS Diagnostic and UBX Configuration GUI Application" license = "BSD-3-Clause" license-files = ["LICENSE"] @@ -44,6 +47,7 @@ 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", 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..46aed9fb 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,6 +90,7 @@ 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() @@ -149,6 +151,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 ) @@ -181,21 +194,23 @@ 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._btn_save.grid(column=1, row=7, columnspan=2, padx=4, pady=3, sticky=E) self._frm_body.columnconfigure(2, weight=1) def _attach_events(self): @@ -208,6 +223,7 @@ def _attach_events(self): self._guirefresh, self._maprefresh, self._logsize, + self._plotchans, self._resizedialog, self._transientdialog, self._mapkey, @@ -224,6 +240,9 @@ 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( @@ -250,6 +269,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") @@ -295,6 +318,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()) 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):