Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/checkpr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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/
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 8 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
]
}
8 changes: 0 additions & 8 deletions CITATION.bib

This file was deleted.

13 changes: 13 additions & 0 deletions CITATION.cff
Original file line number Diff line number Diff line change
@@ -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"
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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). |

---
Expand Down Expand Up @@ -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/) ©.

---
Expand Down
7 changes: 7 additions & 0 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
@@ -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:
Expand Down
42 changes: 23 additions & 19 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion src/pygpsclient/_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@
:license: BSD 3-Clause
"""

__version__ = "1.7.5"
__version__ = "1.7.6"
76 changes: 63 additions & 13 deletions src/pygpsclient/app_config_dialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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()
Expand Down Expand Up @@ -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
)
Expand All @@ -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,
Expand All @@ -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):
Expand All @@ -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)
Expand All @@ -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)

Expand All @@ -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")
Expand All @@ -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:
Expand Down Expand Up @@ -295,13 +335,23 @@ 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())
)
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)
Expand Down
11 changes: 0 additions & 11 deletions src/pygpsclient/attitude_frame.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
:license: BSD 3-Clause
"""

import logging
from tkinter import (
EW,
NSEW,
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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.
Expand Down
1 change: 0 additions & 1 deletion src/pygpsclient/banner_frame.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
"""
Expand Down
Loading