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
10 changes: 4 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ PyGPSClient is a free, open-source, multi-platform graphical GNSS/GPS testing, d
* Provides [NTRIP client](#ntripconfig) facilities for both RTCM3 and SPARTN NTRIP services.
* Can serve as an [NTRIP base station](#basestation) with an RTK-compatible receiver (e.g. u-blox ZED-F9P/ZED-X20P, Quectel LG/LC Series, Septentrio Mosaic Series or Unicore UM9** Series).
* Supports GNSS (*and related*) device configuration via proprietary UBX, NMEA and ASCII TTY protocols, including most u-blox, Quectel, Septentrio, Unicore and Feyman GNSS devices.
* **New in v1.6.7** - Experimental support for [RINEX conversion](#rinex) of raw observation, navigation and meteorology data.
* **New in version >= 1.6.7** - Experimental support for [RINEX conversion](#rinex) of raw observation, navigation and meteorology data.
* Can be installed using the standard `pip` Python package manager - see [installation instructions](#installation) below.

This is an independent project and we have no affiliation whatsoever with any GNSS manufacturer or distributor.
Expand Down Expand Up @@ -183,7 +183,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/master/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/master/src/pygpsclient/resources/iconmonstr-arrow-80-16.png?raw=true)/![expand icon](https://github.com/semuconsulting/PyGPSClient/blob/master/src/pygpsclient/resources/iconmonstr-triangle-1-16.png?raw=true) buttons. Ordinarily 'track:' represents heading of motion (aka course over ground), but the field will display 'hdg:' where static heading (yaw) is available. **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/master/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/master/src/pygpsclient/resources/iconmonstr-arrow-80-16.png?raw=true)/![expand icon](https://github.com/semuconsulting/PyGPSClient/blob/master/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. |
|![console widget](https://github.com/semuconsulting/PyGPSClient/blob/master/images/console_widget.png?raw=true)| Configurable serial console widget showing incoming GNSS data streams in either parsed, binary or tabular hexadecimal formats. Double-right-click to copy contents of console 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/master/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). |
|![levelsview widget](https://github.com/semuconsulting/PyGPSClient/blob/master/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. Double-right-click to toggle levels where C/No = 0 dbHz. |
Expand Down Expand Up @@ -322,7 +322,7 @@ The NTRIP Configuration utility allows users to receive and process NTRIP RTK Co
1. For NTRIP services which require client position data via NMEA GGA sentences, select the appropriate sentence transmission interval in seconds. The default is 'None' (no GGA sentences sent). A value of 10 or 60 seconds is typical.
1. If GGA sentence transmission is enabled, GGA sentences can either be populated from live navigation data (*assuming a receiver is connected and outputting valid position data*) or from fixed reference settings entered in the NTRIP configuration panel (latitude, longitude, elevation and geoid separation - all four reference settings must be provided).
1. To connect to the NTRIP server, click ![connect icon](https://github.com/semuconsulting/PyGPSClient/blob/master/src/pygpsclient/resources/iconmonstr-media-control-48-24.png?raw=true). To disconnect, click ![disconnect icon](https://github.com/semuconsulting/PyGPSClient/blob/master/src/pygpsclient/resources/iconmonstr-media-control-50-24.png?raw=true).
1. If NTRIP data is being successfully received, the banner '**corr:**' status indicator should change to 'YES' and indicate the age and reference station of the correction data (where available) ![dgps status](https://github.com/semuconsulting/PyGPSClient/blob/master/images/dgps_status.png?raw=true). Note that DGPS status is typically maintained for up to 60 seconds after loss of correction signal.
1. If NTRIP data is being successfully received, the banner '**corr:**' status indicator should change to '' and indicate the age and reference station of the correction data (where available) ![dgps status](https://github.com/semuconsulting/PyGPSClient/blob/master/images/dgps_status.png?raw=true). Note that CORR status is typically maintained for up to 60 seconds after loss of correction signal.
1. Some NTRIP services may output RTCM3 or SPARTN correction messages at a high rate, flooding the GUI console display. To suppress these messages in the console, de-select the 'RTCM' or'SPARTN' options in 'Protocols Shown' - the RTCM3 or SPARTN messages will continue to be processed in the background.

Below is a illustrative NTRIP DGPS data log, showing:
Expand Down Expand Up @@ -428,9 +428,7 @@ The RINEX Conversion Dialog supports the conversion of raw observation, navigati

**Pre-Requisites:**

1. A previously-saved binary datalog containing raw observation (UBX RXM-RAWX), navigation (UBX RXM-SFRBX¹) and/or meteorology (NMEA) data or RTCM3 ephemerides (1019, 1020, 1041-1046) messages. A suitable datalog can be recorded using PyGPSClient's [binary datalogging](#datalog) facility. **NB**: The file should contain at least 15-30 minutes of continuous data.

¹ Currently only GPS LNAV/CNAV, Galileo FNAV/INAV and Beidou D1/D2 data is supported by the [pygnssutils RINEX NAV conversion utility](https://github.com/semuconsulting/pygnssutils#rinexconvert). This will be enhanced in future releases.
1. A previously-saved binary datalog containing raw observation (UBX RXM-RAWX), navigation (UBX RXM-SFRBX) and/or meteorology (NMEA) data or RTCM3 ephemerides (1019, 1020, 1041-1046) messages. A suitable datalog can be recorded using PyGPSClient's [binary datalogging](#datalog) facility. **NB**: The file should contain at least 15-30 minutes of continuous data.

**Instructions:**

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.0

1. Can now double-click to toggle between 'hae' (height above ellipsoid) and 'sep' (separation) values in banner panel.
1. Minimum pyubx2 version updated to 1.3.3 (adds new firmware configuration database items for u-blox X20 HPG 2.10).
1. Default log level amended to '0' (ERROR) rather than '-1' (CRITICAL). Any logged exception tracebacks will now appear in terminal logs by default.
1. Updates to RINEX conversion dialog for pygnssutil>=1.2.5 (incorporates various bug fixes to RINEX conversion routines, but remains an experimental facility).

### RELEASE 1.6.10

1. Minor enhancements to UBX Config Dialog confirmation signalling.
Expand Down
Binary file modified images/dgps_status.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 5 additions & 5 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,12 @@ classifiers = [
]

dependencies = [
"requests>=2.28.0",
"Pillow>=9.0.0",
"pygnssutils>=1.2.2",
"requests>=2.34.0",
"Pillow>=12.0.0",
"pygnssutils>=1.2.5",
"pyunigps>=1.0.0",
"pynmeagps>=1.1.4",
"pyubx2>=1.3.1",
"pynmeagps>=1.1.5",
"pyubx2>=1.3.3",
]

[project.scripts]
Expand Down
4 changes: 2 additions & 2 deletions src/pygpsclient/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ def main():
VERBOSITY_DEBUG,
VERBOSITY_CRITICAL,
],
default=VERBOSITY_CRITICAL,
default=VERBOSITY_LOW,
)
ap.add_argument(
"--logtofile",
Expand All @@ -139,7 +139,7 @@ def main():
kwargs = vars(ap.parse_args())

# set up global logging configuration
verbosity = int(kwargs.pop("verbosity", VERBOSITY_CRITICAL))
verbosity = int(kwargs.pop("verbosity", VERBOSITY_LOW))
logtofile = kwargs.pop("logtofile", "")
logger = getLogger(APPNAME) # "pygpsclient"
logger_utils = getLogger("pygnssutils")
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.6.10"
__version__ = "1.7.0"
9 changes: 5 additions & 4 deletions src/pygpsclient/about_dialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from PIL import Image, ImageTk

from pygpsclient.globals import (
CLICK_CURSOR,
ERRCOL,
ICON_APP128,
ICON_GITHUB,
Expand Down Expand Up @@ -112,7 +113,7 @@ def _body(self):
self._frm_body,
text="",
width=16,
cursor="hand2",
cursor=CLICK_CURSOR,
)
self._chk_checkupdate = Checkbutton(
self._frm_body,
Expand All @@ -122,18 +123,18 @@ def _body(self):
self._lbl_sponsoricon = Label(
self._frm_body,
image=self._img_sponsor,
cursor="hand2",
cursor=CLICK_CURSOR,
)
self._lbl_github = Label(
self._frm_body,
text=GITHUB_URL,
fg=INFOCOL,
cursor="hand2",
cursor=CLICK_CURSOR,
)
self._lbl_copyright = Label(
self._frm_body,
text=COPYRIGHT,
cursor="hand2",
cursor=CLICK_CURSOR,
)

def _do_layout(self):
Expand Down
17 changes: 13 additions & 4 deletions src/pygpsclient/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,8 @@
SOCKSERVER_MAX_CLIENTS,
SPARTN_EVENT,
SPARTN_PROTOCOL,
STATUSPRIORITY,
STATUS_PRIORITY,
STATUS_TIMEOUT,
TTY_PROTOCOL,
UNDO,
)
Expand Down Expand Up @@ -225,7 +226,9 @@ def __init__(self, master, **kwargs):
self.frm_settings = None
self._conn_status = DISCONNECTED
self._rtk_conn_status = DISCONNECTED
self._last_gui_update = datetime.now()
now = datetime.now()
self._last_gui_update = now
self._last_status_update = now
self._socket_thread = None
self._socket_server = None
self.consoledata = []
Expand Down Expand Up @@ -969,7 +972,8 @@ def process_data(self, raw_data: bytes, parsed_data: object, marker: str = ""):
self.consoledata.append((raw_data, parsed_data, marker))

# periodically update widgets if visible
if datetime.now() > self._last_gui_update + timedelta(
now = datetime.now()
if now > self._last_gui_update + timedelta(
seconds=self.configuration.get("guiupdateinterval_f")
):
self._refresh_widgets()
Expand All @@ -978,6 +982,10 @@ def process_data(self, raw_data: bytes, parsed_data: object, marker: str = ""):
self.sqlite_handler.load_data()
self._last_gui_update = datetime.now()

# remove stale status messages
if now > self._last_status_update + timedelta(seconds=STATUS_TIMEOUT):
self.status_label = ("", INFOCOL)

# update GPX track file if enabled
if self.configuration.get("recordtrack_b"):
self.file_handler.update_gpx_track()
Expand Down Expand Up @@ -1165,7 +1173,7 @@ def status_label(self, message: str | tuple[str, str]):
"""

def priority(col):
return STATUSPRIORITY.get(col, 0)
return STATUS_PRIORITY.get(col, 0)

if isinstance(message, tuple):
message, color = message
Expand All @@ -1180,6 +1188,7 @@ def priority(col):
color = INFOCOL if color == "blue" else color
self.status_label.config(text=message, fg=color)
self.status_label.update()
self._last_status_update = datetime.now()
else: # defer message until frm_status is instantiated
if isinstance(self._deferredmsg, tuple):
defpty = priority(self._deferredmsg[1])
Expand Down
36 changes: 31 additions & 5 deletions src/pygpsclient/banner_frame.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

from pygpsclient.globals import (
BGCOL,
CLICK_CURSOR,
CONNECTED,
CONNECTED_FILE,
CONNECTED_NTRIP,
Expand Down Expand Up @@ -94,6 +95,7 @@ def __init__(self, app: Frame, parent: Frame, *args, **kwargs):
self._img_ntrip = ImageTk.PhotoImage(Image.open(ICON_NTRIPCONFIG))
self._img_spartn = ImageTk.PhotoImage(Image.open(ICON_SPARTNCONFIG))
self._img_blank = ImageTk.PhotoImage(Image.open(ICON_BLANK))
self._sep = False

super().__init__(parent, *args, **kwargs)

Expand Down Expand Up @@ -143,12 +145,18 @@ def _body(self):
height=22,
command=self._toggle_advanced,
image=self._img_expand,
cursor=CLICK_CURSOR,
)
self._lbl_lhmsl = Label(
self._frm_advanced, text="hmsl:", bg=BGCOL, fg=FGCOL, anchor=N
)
self._lbl_lhae = Label(
self._frm_advanced, text="hae:", bg=BGCOL, fg=FGCOL, anchor=N
self._frm_advanced,
text="hae:",
bg=BGCOL,
fg=FGCOL,
anchor=N,
cursor=CLICK_CURSOR,
)
self._lbl_lspd = Label(
self._frm_advanced, text="speed:", bg=BGCOL, fg=FGCOL, anchor=N
Expand Down Expand Up @@ -195,7 +203,12 @@ def _body(self):
self._frm_advanced, bg=BGCOL, fg="orange", width=13, anchor=W
)
self._lbl_hae = Label(
self._frm_advanced, bg=BGCOL, fg="orange", width=13, anchor=W
self._frm_advanced,
bg=BGCOL,
fg="orange",
width=13,
anchor=W,
cursor=CLICK_CURSOR,
)
self._lbl_spd = Label(
self._frm_advanced, bg=BGCOL, fg="deepskyblue", width=12, anchor=W
Expand Down Expand Up @@ -283,6 +296,8 @@ def _attach_events(self):
"""

self.bind("<Configure>", self._on_resize)
self._lbl_lhae.bind("<Double-Button-1>", self._on_sep)
self._lbl_hae.bind("<Double-Button-1>", self._on_sep)

def _toggle_advanced(self):
"""
Expand Down Expand Up @@ -396,7 +411,8 @@ def _update_pos(self, pos_format, units):
self._lbl_llat.config(text="lat:")
self._lbl_llon.config(text="lon:")
self._lbl_lhmsl.config(text="hmsl:")
self._lbl_lhae.config(text="hae:")
lhae = "sep:" if self._sep else "hae:"
self._lbl_lhae.config(text=lhae)
alt_u = "ft" if units in (UI, UIK) else "m"

try:
Expand All @@ -423,7 +439,8 @@ def _update_pos(self, pos_format, units):
self._lbl_lat.config(text=f"{lat:{deg_f}}")
self._lbl_lon.config(text=f"{lon:{deg_f}}")
self._lbl_hmsl.config(text=f"{alt:.4f} {alt_u}")
self._lbl_hae.config(text=f"{hae:.4f} {alt_u}")
haev = hae - alt if self._sep else hae
self._lbl_hae.config(text=f"{haev:.4f} {alt_u}")
except (TypeError, ValueError):
self._lbl_lat.config(text=NA)
self._lbl_lon.config(text=NA)
Expand Down Expand Up @@ -623,9 +640,18 @@ def _set_fontsize(self):
):
ctl.config(font=scale_font(self.width, FONTBASE - 4, FONTSCALE)[0])

def _on_sep(self, event): # pylint: disable=unused-argument
"""
Toggle hae/sep display.

:param event event: mouse click event
"""

self._sep = not self._sep

def _on_resize(self, event): # pylint: disable=unused-argument
"""
Resize frame
Resize frame.

:param event event: resize event
"""
Expand Down
3 changes: 3 additions & 0 deletions src/pygpsclient/dynamic_config_frame.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
)

from pygpsclient.globals import (
CLICK_CURSOR,
ERRCOL,
INFOCOL,
NMEA_CFGOTHER,
Expand Down Expand Up @@ -207,13 +208,15 @@ def _body(self):
width=50,
command=self._on_set_cfg,
font=self.__app.font_md,
cursor=CLICK_CURSOR,
)
self._btn_refresh = Button(
self,
image=self.__container.img_redraw,
width=40,
command=self._on_refresh,
font=self.__app.font_md,
cursor=CLICK_CURSOR,
)
self._lbl_command = Label(self, text="", anchor=W)
self._frm_container = Frame(self)
Expand Down
4 changes: 3 additions & 1 deletion src/pygpsclient/globals.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
)
BSR = "backslashreplace"
CLASS = "cls"
CLICK_CURSOR = "hand2" # cursor displayed for clickable widgets
CMDINITDELAY = 3000 # initial delay before sending commands to new connection
CMDPAUSE = 100 # pause between sent commands in ms
COLORTAGS = "colortags"
Expand Down Expand Up @@ -246,7 +247,8 @@
SPARTN_SOURCE_LB = 1
SPONSOR_URL = "https://buymeacoffee.com/semuconsulting"
SQRT2 = 0.7071067811865476 # square root of 2
STATUSPRIORITY = {INFOCOL: 0, "blue": 0, OKCOL: 2, "green": 1, ERRCOL: 3, "red": 3}
STATUS_PRIORITY = {INFOCOL: 0, "blue": 0, OKCOL: 2, "green": 1, ERRCOL: 3, "red": 3}
STATUS_TIMEOUT = 15 # timeout for stale status messages in seconds
TIME0 = datetime(1970, 1, 1) # basedate for time()
TIMEOUTS = ("0.1", "0.2", "0.5", "1", "2", "5", "10", "20", "None", "0")
# map nmea talker to gnss_id
Expand Down
3 changes: 3 additions & 0 deletions src/pygpsclient/gpx_dialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
from pygpsclient.canvas_subclasses import CanvasGraph
from pygpsclient.globals import (
BGCOL,
CLICK_CURSOR,
CUSTOM,
ERRCOL,
FGCOL,
Expand Down Expand Up @@ -170,6 +171,7 @@ def _body(self):
image=self.img_load,
width=40,
command=self._on_load,
cursor=CLICK_CURSOR,
)
self._lbl_maptype = Label(self._frm_controls, text="Map Type")
self._spn_maptype = Spinbox(
Expand Down Expand Up @@ -203,6 +205,7 @@ def _body(self):
image=self.img_redraw,
width=40,
command=self._on_redraw,
cursor=CLICK_CURSOR,
)

def _do_layout(self):
Expand Down
4 changes: 4 additions & 0 deletions src/pygpsclient/importmap_dialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
from pygpsclient.canvas_map import CanvasMap
from pygpsclient.globals import (
BGCOL,
CLICK_CURSOR,
ERRCOL,
IMPORT,
INFOCOL,
Expand Down Expand Up @@ -92,18 +93,21 @@ def _body(self):
image=self.img_load,
width=40,
command=self._on_load,
cursor=CLICK_CURSOR,
)
self._btn_import = Button(
self._frm_controls,
image=self.img_send,
width=40,
command=self._on_import,
cursor=CLICK_CURSOR,
)
self._btn_redraw = Button(
self._frm_controls,
image=self.img_redraw,
width=40,
command=self._on_redraw,
cursor=CLICK_CURSOR,
)
self._chk_first = Checkbutton(
self._frm_controls, text="First?", variable=self._first
Expand Down
Loading