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
7 changes: 5 additions & 2 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": ".",
Expand Down
18 changes: 16 additions & 2 deletions .vscode/tasks.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": []
},
{
Expand All @@ -45,6 +56,9 @@
"args": [
"~/pygpsclient/bin/activate",
],
"windows": {
"command": "~/pygpsclient/Scripts/activate"
},
"problemMatcher": [],
},
{
Expand Down Expand Up @@ -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": {
Expand Down
2 changes: 1 addition & 1 deletion INSTALLATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ The exact location of the site_packages and binary directories will depend on th

### <a name="gil">free-threading vs global interpreter lock (GIL)</a>

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
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.|
Expand Down
6 changes: 6 additions & 0 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
5 changes: 0 additions & 5 deletions examples/F9P_3D_printed_case.md

This file was deleted.

9 changes: 9 additions & 0 deletions examples/Sparkfun_GNSS_case_3Dprinter.md
Original file line number Diff line number Diff line change
@@ -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)
18 changes: 16 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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",
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.8"
__version__ = "1.7.9"
1 change: 1 addition & 0 deletions src/pygpsclient/gnss_status.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
81 changes: 49 additions & 32 deletions src/pygpsclient/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
"""
Expand All @@ -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
Expand Down Expand Up @@ -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:
"""
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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:
Expand Down
7 changes: 4 additions & 3 deletions src/pygpsclient/init_presets.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
22 changes: 18 additions & 4 deletions src/pygpsclient/levelsview_frame.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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(
Expand All @@ -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
Expand Down
2 changes: 2 additions & 0 deletions src/pygpsclient/nmea_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
"""
Expand Down
Loading
Loading