diff --git a/INSTALLATION.md b/INSTALLATION.md
index 36c21417..c78e7caf 100644
--- a/INSTALLATION.md
+++ b/INSTALLATION.md
@@ -41,7 +41,7 @@ There are two file formats in common use for distributing Python packages:
By default, pip will look for wheel first, and only resort to using sdist where no wheel is available for the target platform.
-For pure Python packages like PyGPSClient and its subsidiary [GNSS utilities](https://github.com/semuconsulting), which contain no non-Python extension modules, the differences between sdist and wheel distributions are to some extent academic. The distinction is, however, relevant for some of PyGPSClient's optional dependencies (e.g. `cryptography` and `rasterio`) - see [troubleshooting](#troubleshooting) for further details.
+For pure Python packages like PyGPSClient and its subsidiary [GNSS utilities](https://github.com/semuconsulting), which contain no non-Python extension modules, the differences between sdist and wheel distributions are to some extent academic - a single pygpsclient `pygpsclient-*-none-any.whl` package can be installed on *any* target architecture. The distinction is, however, relevant for some of PyGPSClient's optional dependencies (e.g. `cryptography` and `rasterio`) - see [troubleshooting](#troubleshooting) for further details.
### site_packages and binaries directories
diff --git a/README.md b/README.md
index d5154b5b..cfc10a13 100644
--- a/README.md
+++ b/README.md
@@ -35,6 +35,14 @@ This is an independent project and we have no affiliation whatsoever with any GN
*Screenshot showing mixed-protocol stream from u-blox ZED-F9P receiver, using PyGPSClient's [NTRIP Client](#ntripconfig) with a base station 26 km to the west to achieve better than 2 cm accuracy*
+#### References
+
+1. [Glossary of GNSS Terms and Abbreviations](https://www.semuconsulting.com/gnsswiki/glossary/).
+1. [GNSS Positioning - A Reviser](https://www.semuconsulting.com/gnsswiki/) - a general overview of GNSS, OSR, SSR, RTK, NTRIP and SPARTN positioning and error correction technologies and terminology.
+1. [Achieving cm Level GNSS Accuracy using RTK](https://www.semuconsulting.com/gnsswiki/rtktips/) - practical tips on high precision RTK using PyGPSClient.
+1. From time to time, instructional videos may be posted to the [semuadmin YouTube channel](https://www.youtube.com/@semuadmin).
+1. [Sphinx API Documentation](https://www.semuconsulting.com/pygpsclient) in HTML format.
+
---
## Current Status
@@ -46,13 +54,7 @@ This is an independent project and we have no affiliation whatsoever with any GN


-
-The PyGPSClient home page is at [PyGPSClient](https://github.com/semuconsulting/PyGPSClient). The following references may be useful:
-1. [Glossary of GNSS Terms and Abbreviations](https://www.semuconsulting.com/gnsswiki/glossary/).
-1. [GNSS Positioning - A Reviser](https://www.semuconsulting.com/gnsswiki/) - a general overview of GNSS, OSR, SSR, RTK, NTRIP and SPARTN positioning and error correction technologies and terminology.
-1. [Achieving cm Level GNSS Accuracy using RTK](https://www.semuconsulting.com/gnsswiki/rtktips/) - practical tips on high precision RTK using PyGPSClient.
-1. [Sphinx API Documentation](https://www.semuconsulting.com/pygpsclient) in HTML format.
-1. From time to time, instructional videos may be posted to the [semuadmin YouTube channel](https://www.youtube.com/@semuadmin).
+The PyGPSClient home page is at [PyGPSClient](https://github.com/semuconsulting/PyGPSClient).
Contributions welcome - please refer to [CONTRIBUTING.MD](https://github.com/semuconsulting/PyGPSClient/blob/master/CONTRIBUTING.md).
@@ -116,8 +118,8 @@ For more comprehensive installation instructions, please refer to [INSTALLATION.
1. File Delay - Select delay in milliseconds between individual reads when streaming from binary file (default 20 milliseconds).
1. Tags - Enable color tags in console (see Console Widget below).
1. Position Format and Units - Change the displayed position (D.DD / D.M.S / D.M.MM / ECEF) and unit (metric/imperial) formats.
-1. Show Unused Satellites - Include or exclude satellites that are not used in the navigation solution (e.g. because their signal level is too low) from the graph and sky view panels.
-1. DataLogging - Turn Data logging in the selected format on or off. On first selection, you will be prompted to select the directory into which timestamped log files are saved.
+1. Include C/N0 = 0 - Include or exclude satellites where carrier to noise ratio (C/N0) = 0.
+1. DataLogging - Turn Data logging in the selected format (Binary, Parsed, Hex Tabular, Hex String, Parsed+Hex Tabular) on or off. On first selection, you will be prompted to select the directory into which timestamped log files are saved. Log files are cycled when a maximum size is reached (default is 10 MB, manually configurable via `logsize_n` setting).
1. GPX Track - Turn track recording (in GPX format) on or off. On first selection, you will be prompted to select the directory into which timestamped GPX track files are saved.
1. Database - Turn spatialite database recording (*where available*) on or off. On first selection, you will be prompted to select the directory into which the `pygpsclient.sqlite` database is saved. Note that, when first created, the database's spatial metadata will take a few seconds to initialise (*up to a minute on Raspberry Pi and similar SBC*). **NB** This facility is dependent on your Python environment supporting the requisite [sqlite3 `mod_spatialite` extension](https://www.gaia-gis.it/fossil/libspatialite/index) - see [INSTALLATION.md](https://github.com/semuconsulting/PyGPSClient/blob/master/INSTALLATION.md#prereqs) for further details. If not supported, the option will be greyed out. Check the Menu..Help..About dialog for an indication of the current spatialite support status.
@@ -127,10 +129,10 @@ For more comprehensive installation instructions, please refer to [INSTALLATION.
**NB** Any active serial or RTK connection must be stopped before loading a new configuration.
1. [Socket Server / NTRIP Caster](#socketserver) facility with two modes of operation: (a) open, unauthenticated Socket Server or (b) NTRIP Caster (mountpoint = `pygnssutils`).
1. [UBX Configuration Dialog](#ubxconfig), with the ability to send a variety of UBX CFG configuration commands to u-blox GNSS devices. This includes the facility to add **user-defined commands or command sequences** - see instructions under [user-defined presets](#userdefined) below. To display the UBX Configuration Dialog (*only functional when connected to a UBX GNSS device via serial port*), click
-, or go to Menu..Options..UBX Configuration Dialog.
-1. [NMEA Configuration Dialog](#nmeaconfig), with the ability to send a variety of NMEA configuration commands to GNSS devices (e.g. Quectel LG290P). This includes the facility to add **user-defined commands or command sequences** - see instructions under [user-defined presets](#userdefined) below. To display the NMEA Configuration Dialog (*only functional when connected to a compatible GNSS device via serial port*), click , or go to Menu..Options..NMEA Configuration Dialog.
+, or go to Menu..Options..UBX Configuration Dialog.
+1. [NMEA Configuration Dialog](#nmeaconfig), with the ability to send a variety of NMEA configuration commands to GNSS devices (e.g. Quectel LG290P). This includes the facility to add **user-defined commands or command sequences** - see instructions under [user-defined presets](#userdefined) below. To display the NMEA Configuration Dialog (*only functional when connected to a compatible GNSS device via serial port*), click , or go to Menu..Options..NMEA Configuration Dialog.
1. [TTY Config Dialog](#ttycommands), with the ability to send a variety of TTY (ASCII) configuration commands to GNSS and related devices (e.g. Septentrio X5, Feyman IM19). This includes the facility to add **user-defined commands or command sequences** - see instructions under [user-defined presets](#userdefined) below. To display the TTY Commands Dialog (*only functional when connected to a compatible GNSS device via serial port*), click
-, or go to Menu..Options..TTY Commands.
+, or go to Menu..Options..TTY Commands.
1. [NTRIP Client](#ntripconfig) facility with the ability to connect to a specified NTRIP caster, parse the incoming RTCM3 or SPARTN data and feed this data to a compatible GNSS receiver (*requires an Internet connection and access to an NTRIP caster and local mountpoint*). To display the NTRIP Client Configuration Dialog, click
, or go to Menu..Options..NTRIP Configuration Dialog.
1. [SPARTN Client](#spartnconfig) facility with the ability to configure an IP or L-Band SPARTN Correction source and SPARTN-compatible GNSS receiver (e.g. ZED-F9P) and pass the incoming correction data to the GNSS receiver (*requires an Internet connection and access to a SPARTN location service*). To display the SPARTN Client Configuration Dialog, go to Menu..Options..SPARTN Configuration Dialog.
@@ -138,7 +140,7 @@ For more comprehensive installation instructions, please refer to [INSTALLATION.
#### Configuration settings
-- Configuration settings for PyGPSClient can be saved and recalled via the Menu..File..Save Configuration and Menu..File..Load Configuration options. By default, PyGPSClient will look for a file named `pygpsclient.json` in the user's home directory. Certain configuration settings require manual editing e.g. custom preset UBX, NMEA and TTY commands and tag colour schemes - see details below.
+- Configuration settings for PyGPSClient can be saved and recalled via the Menu..File..Save Configuration and Menu..File..Load Configuration options. By default, PyGPSClient will look for a file named `pygpsclient.json` in the user's home directory. Certain configuration settings require manual editing e.g. custom preset UBX, NMEA and TTY commands and tag colour schemes - see details below. It is recommended to re-save the configuration settings after each PyGPSClient version update, or if you see the warning "Consider re-saving" on startup.
#### Checking for the latest version
@@ -150,20 +152,20 @@ For more comprehensive installation instructions, please refer to [INSTALLATION.
#### Transient dialog setting
-- A boolean configuration setting `transient_dialog_b` governs whether pop-up dialogs are 'transient' (i.e. always on top of main application dialog) or not. The default setting of `0` allows pop-up dialogs to be minimised independently of the main application window, but be mindful that some dialogs may end up hidden behind others e.g. "Open file/folder" dialogs. **If a file open button appears unresponsive, check that the "Open file/folder" panel isn't already open but obscured**.
+- A boolean configuration setting `transient_dialog_b` governs whether pop-up dialogs are 'transient' (i.e. always on top of main application dialog) or not. Changing this setting to `0` allows pop-up dialogs to be minimised independently of the main application window, but be mindful that some dialogs may end up hidden behind others e.g. "Open file/folder" dialogs. **If a file open button appears unresponsive, check that the "Open file/folder" panel isn't already open but obscured**. If you're accessing the desktop via a VNC session (e.g. to a headless Raspberry Pi) it is recommended to keep the setting at the default `1`, as VNC may not recognise keystrokes on overlaid transient windows.
---
| User-selectable 'widgets' | To show or hide the various widgets, go to Menu..View and click on the relevant hide/show option. |
|---------------------------|---------------------------------------------------------------------------------------------------|
-|| 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 / buttons. **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 |
+|| 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 / buttons. **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 |
|| 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 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 showing current satellite carrier-to-noise (CNo) levels for each GNSS constellation. Double-click to toggle legend. |
+|| Levels view widget showing current satellite carrier-to-noise (CNo) levels for each GNSS constellation. Double-click to toggle legend. |
|| Map widget with various modes of display - select from "map" / "sat" (online) or "world" / "custom" (offline). Select zoom level 1 - 20. Double-click the zoom level label to reset the zoom to 10. Double-right-click the zoom label to maximise zoom to 20. Tick Track to show track (track will only be recorded while this box is checked). Double-Right-click will clear the map. Map Type = 'world': a static offline Mercator world map showing current global location.
|| Map Type = 'map', 'sat' or 'hyb' (hybrid): Dynamic, online web map or satellite image via MapQuest API (*requires an Internet connection and free [Mapquest API Key](#mapquestapi)*). By default, the web map will automatically refresh every 60 seconds (*indicated by a small timer icon at the top left*). The default refresh rate can be amended by changing the `"mapupdateinterval_n":` value in your json configuration file, but **NB** the facility is not intended to be used for real-time navigation. Double-click anywhere in the map to immediately refresh. |
-|| Map Type = 'custom': One or more custom geo-referenced offline maps can be imported using the Menu..Options..Import Custom Map facility, or by manually setting the `usermaps_l` field in the json configuration file. The `usermaps_l` setting represents a list of map paths and bounding boxes in the format ["path to map image", [minlat, minlon, maxlat, maxlon]] - see [example configuration file](https://github.com/semuconsulting/PyGPSClient/blob/master/pygpsclient.json#L281). Map images must be a [supported format](https://pillow.readthedocs.io/en/stable/handbook/image-file-formats.html) and use a standard WGS84 Web Mercator projection e.g. EPSG:4326. PyGPSClient will automatically select the first image whose extents encompass the current location, based on the order in which the maps appear in `usermaps_l`. NB: The minimum and maximum viable 'zoom' levels depend on the resolution and extents of the imported image and the user's display - if the zoom bounds exceed the image extents, the Zoom spinbox will be highlighted. |
-|| Import Custom Map dialog. Click  to open the custom map image location (*the default file suffix is `*.tif` - select Show Options to select any file suffix `*.*`*). If the `rasterio` library is installed and the image is geo-referenced (e.g. using [QGIS](https://qgis.org/)), the map extents will be automatically extracted - otherwise they must be entered manually. Import the custom map path and extent settings by clicking . By default, the imported map will be appended to the existing list - click 'First?' to insert the map at the top of the list instead. See [Creating Custom Maps for PyGPSClient](https://www.semuconsulting.com/gnsswiki/custommapwiki/) for tips on how to create a suitable georeferenced map image.|
-|| Spectrum widget showing a spectrum analysis chart (*GNSS receiver must be capable of outputting UBX MON-SPAN messages*). Clicking anywhere in the spectrum chart will display the frequency and decibel reading at that point. Double-clicking anywhere in the chart will toggle the GNSS frequency band markers (L1, G2, etc.) on or off. Right-click anywhere in the chart to capture a snapshot of the spectrum data, which will then be superimposed on the live data. Double-right-click to clear snapshot. **NB:** Some receivers (e.g. NEO-F10N) will not output the requisite MON-SPAN messages unless the port baud rate is at least 57,600. |
+|| Map Type = 'custom': One or more user-defined offline geo-referenced map images can be imported using the Menu..Options..Import Custom Map facility, or by manually setting the `usermaps_l` field in the json configuration file. The `usermaps_l` setting represents a list of map paths and extents in the format ["path to map image", [minlat, minlon, maxlat, maxlon]] - see [example configuration file](https://github.com/semuconsulting/PyGPSClient/blob/master/pygpsclient.json#L281). Map images must be a [supported format](https://pillow.readthedocs.io/en/stable/handbook/image-file-formats.html) and use a standard WGS84 Web Mercator projection e.g. EPSG:4326. PyGPSClient will automatically select the first map whose extents encompass the current location, based on the order in which the maps appear in `usermaps_l`. NB: The minimum and maximum viable 'zoom' levels depend on the resolution and extents of the imported image and the user's display - if the zoom bounds exceed the image extents, the Zoom spinbox will be highlighted. Offline and online zoom levels will not necessarily correspond. |
+|| Import Custom Map dialog. Click  to open the custom map image location (*the default file suffix is `*.tif` - select Show Options to select any file suffix `*.*`*). If the `rasterio` library is installed and the image is geo-referenced (e.g. using [QGIS](https://qgis.org/)), the map extents will be automatically extracted - otherwise they must be entered manually. Import the custom map path and extent settings by clicking . By default, the imported map will be appended to the existing list - click 'First?' to insert the map at the top of the list instead. See [Creating Custom Maps for PyGPSClient](https://www.semuconsulting.com/gnsswiki/custommapwiki/) for tips on how to create a suitable geo-referenced map image.|
+|| Spectrum widget showing a spectrum analysis chart (*GNSS receiver must be capable of outputting UBX MON-SPAN messages*). Clicking anywhere in the spectrum chart will display the frequency and decibel reading at that point. Double-clicking anywhere in the chart will toggle the GNSS frequency band markers (L1, G2, etc.) on or off. Right-click anywhere in the chart to capture a snapshot of the spectrum data, which will then be superimposed on the live data (*this can, for example, be used to compare reception with different antenna configurations*). Double-right-click to clear snapshot. **NB:** Some receivers (e.g. NEO-F10N) will not output the requisite MON-SPAN messages unless the port baud rate is at least 57,600. |
|| 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 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 plots the relative 2D position, track and status information for the roving receiver in a fixed or moving base / rover RTK configuration. Can also display relative position of NTRIP mountpoint and receiver in a static RTK configuration. Double-click to clear existing plot. |
@@ -413,7 +415,7 @@ Configure NAV-STATUS Message Rate on ZED-F9P, CFG, CFG-MSG, 0103000100000000, 1
Configure UART baud rate on LG290P; P; QTMCFGUART; W,460800; 1
```
-Multiple commands can be concatenated on a single line. Illustrative examples are shown in the sample [pygpsclient.json](https://github.com/semuconsulting/PyGPSClient/blob/master/pygpsclient.json#L174) file.
+Multiple commands can be concatenated on a single line. Illustrative examples are shown in the sample [pygpsclient.json](https://github.com/semuconsulting/PyGPSClient/blob/master/pygpsclient.json#L188) file.
---
## Command Line Utilities
diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md
index 522e27be..642f9ffa 100644
--- a/RELEASE_NOTES.md
+++ b/RELEASE_NOTES.md
@@ -1,5 +1,17 @@
# PyGPSClient Release Notes
+### RELEASE 1.5.22
+
+FIXES:
+
+1. Fix KeyError in UBX Legacy Dynamic Config Panel https://github.com/semuconsulting/PyGPSClient/issues/227
+
+ENHANCEMENTS:
+
+1. Make maximum individual data log file size (in bytes) manually configurable via `logsize_n` setting in json configuration file. Default is 10 MB.
+1. Tolerate unrecognised configuration settings in json file with warning "Consider re-saving" (previously json file would have been rejected). Unrecognised settings will be logged as INFO messages (--verbosity 2).
+1. Internal enhancements to thread handling to improve dialog response at high transaction rates.
+
### RELEASE 1.5.21
FIXES:
diff --git a/images/app.png b/images/app.png
index b346355e..b40123df 100644
Binary files a/images/app.png and b/images/app.png differ
diff --git a/pygpsclient.json b/pygpsclient.json
index 653e72a4..c23eba3b 100644
--- a/pygpsclient.json
+++ b/pygpsclient.json
@@ -1,5 +1,5 @@
{
- "version_s": "1.5.19",
+ "version_s": "1.5.21",
"Banner": true,
"Settings": true,
"Status": true,
@@ -47,9 +47,10 @@
"showtrack_b": 0,
"legend_b": 1,
"unusedsat_b": 0,
- "logformat_s": "Binary",
"datalog_b": 0,
+ "logformat_s": "Binary",
"logpath_s": "",
+ "logsize_n": 10485760,
"recordtrack_b": 0,
"trackpath_s": "",
"database_b": 0,
@@ -148,7 +149,6 @@
"scatterlon_f": -115.81513
},
"imusettings_d": {
- "source_s": "ESF-ALG",
"range_n": 30,
"option_s": "N/A"
},
diff --git a/pyproject.toml b/pyproject.toml
index 863bc0b2..b5c333c4 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -49,11 +49,7 @@ classifiers = [
"Topic :: Scientific/Engineering :: GIS",
]
-dependencies = [
- "requests>=2.28.0",
- "Pillow>=9.0.0",
- "pygnssutils>=1.1.20"
-]
+dependencies = ["requests>=2.28.0", "Pillow>=9.0.0", "pygnssutils>=1.1.20"]
[project.scripts]
pygpsclient = "pygpsclient.__main__:main"
@@ -65,10 +61,7 @@ repository = "https://github.com/semuconsulting/PyGPSClient"
changelog = "https://github.com/semuconsulting/PyGPSClient/blob/master/RELEASE_NOTES.md"
[dependency-groups]
-optional = [
- "rasterio",
- "cryptography",
-]
+optional = ["rasterio", "cryptography"]
build = [
"awscli",
"build",
@@ -141,7 +134,7 @@ disable = """
[tool.pytest.ini_options]
minversion = "7.0"
-addopts = "--cov --cov-report html --cov-fail-under 18"
+addopts = "--cov --cov-report html --cov-fail-under 17"
pythonpath = ["src"]
testpaths = ["tests"]
@@ -152,7 +145,7 @@ source = ["src"]
source = ["src"]
[tool.coverage.report]
-fail_under = 18
+fail_under = 17
[tool.coverage.html]
directory = "htmlcov"
diff --git a/src/pygpsclient/__main__.py b/src/pygpsclient/__main__.py
index 96460ef6..b8bd5b8b 100644
--- a/src/pygpsclient/__main__.py
+++ b/src/pygpsclient/__main__.py
@@ -1,5 +1,5 @@
"""
-Entry point for PyGPSClient Application.
+CLI Entry point for PyGPSClient Application.
Created on 12 Sep 2020
diff --git a/src/pygpsclient/_version.py b/src/pygpsclient/_version.py
index b15456cd..3d6e45dc 100644
--- a/src/pygpsclient/_version.py
+++ b/src/pygpsclient/_version.py
@@ -8,4 +8,4 @@
:license: BSD 3-Clause
"""
-__version__ = "1.5.21"
+__version__ = "1.5.22"
diff --git a/src/pygpsclient/about_dialog.py b/src/pygpsclient/about_dialog.py
index 3aaa5c0f..22d2c888 100644
--- a/src/pygpsclient/about_dialog.py
+++ b/src/pygpsclient/about_dialog.py
@@ -235,7 +235,7 @@ def _check_for_update(self, *args, **kwargs): # pylint: disable=unused-argument
Check for updates.
"""
- self.set_status("")
+ self.status_label = ""
self._updates = []
for i, (nam, current) in enumerate(LIBVERSIONS.items()):
latest = check_latest(nam)
@@ -280,4 +280,4 @@ def _brew_warning(self):
Display warning that some functionality unavailable with Homebrew.
"""
- self.set_status(BREWWARN, INFOCOL)
+ self.status_label = (BREWWARN, INFOCOL)
diff --git a/src/pygpsclient/app.py b/src/pygpsclient/app.py
index a8b8a795..260735e1 100644
--- a/src/pygpsclient/app.py
+++ b/src/pygpsclient/app.py
@@ -3,13 +3,17 @@
PyGPSClient - Main tkinter application class.
+Essentially the 'Model' in a nominal MVC (Model-View-Controller)
+architecture.
+
- Loads configuration from json file (if available)
- Instantiates all frames, widgets, and protocol handlers.
-- Starts and stops threaded dialog and protocol handler processes.
-- Maintains current serial and RTK connection status.
-- Reacts to various message events, processes navigation data
- placed on input message queue by serial, socket or file stream reader
- and assigns to appropriate NMEA, UBX or RTCM protocol handler.
+- Maintains state of all user-selectable widgets.
+- Maintains state of all threaded dialog and protocol handler processes.
+- Maintains state of serial and RTK connections.
+- Handles event-driven data processing of navigation data placed on
+ input message queue by stream handler and assigns to appropriate
+ protocol handler.
- Maintains central dictionary of current key navigation data as
`gnss_status`, for use by user-selectable widgets.
@@ -29,7 +33,7 @@
:license: BSD 3-Clause
"""
-# pylint: disable=too-many-ancestors, no-member
+# pylint: disable=too-many-ancestors, no-member, too-many-lines
import logging
from datetime import datetime, timedelta
@@ -39,7 +43,9 @@
from subprocess import CalledProcessError, run
from sys import executable
from threading import Thread
-from tkinter import E, Frame, N, PhotoImage, S, Tk, Toplevel, W, font
+from time import process_time_ns, time
+from tkinter import NSEW, Frame, Label, PhotoImage, Tk, Toplevel, font
+from types import NoneType
from pygnssutils import GNSSMQTTClient, GNSSNTRIPClient, MQTTMessage
from pygnssutils.gnssreader import (
@@ -64,7 +70,6 @@
from pygpsclient.dialog_state import DialogState
from pygpsclient.file_handler import FileHandler
from pygpsclient.globals import (
- CFG,
CLASS,
CONFIGFILE,
CONNECTED,
@@ -90,7 +95,6 @@
SPARTN_EVENT,
SPARTN_PROTOCOL,
STATUSPRIORITY,
- THD,
TTY_PROTOCOL,
)
from pygpsclient.gnss_status import GNSSStatus
@@ -155,6 +159,9 @@ def __init__(self, master, **kwargs): # pylint: disable=too-many-statements
:param kwargs: optional (CLI) kwargs
"""
+ self.starttime = time() # for run time benchmarking
+ self.processtime = 0 # for process time benchmarking
+
self.__master = master
self.logger = logging.getLogger(__name__)
@@ -195,8 +202,6 @@ def __init__(self, master, **kwargs): # pylint: disable=too-many-statements
self._last_gui_update = datetime.now()
self._socket_thread = None
self._socket_server = None
- self._colcount = 0
- self._rowcount = 0
self.consoledata = []
# load config from json file
@@ -206,8 +211,9 @@ def __init__(self, master, **kwargs): # pylint: disable=too-many-statements
self.configuration.loadcli(**kwargs)
if configerr == "":
self.update_widgets() # set initial widget state
- if self._nowidgets: # if all widgets have been disabled in config
- self.set_status(NOWDGSWARN.format(configfile), ERRCOL)
+ # warning if all widgets have been disabled in config
+ if self._nowidgets:
+ self.status_label = (NOWDGSWARN.format(configfile), ERRCOL)
# open database if database recording enabled
dbpath = self.configuration.get("databasepath_s")
@@ -222,7 +228,7 @@ def __init__(self, master, **kwargs): # pylint: disable=too-many-statements
self._do_layout()
self._attach_events()
- # initialise widgets
+ # instantiate widgets
for value in self.widget_state.state.values():
frm = getattr(self, value[FRAME])
if hasattr(frm, "init_frame"):
@@ -232,7 +238,7 @@ def __init__(self, master, **kwargs): # pylint: disable=too-many-statements
# display initial connection status
self.frm_banner.update_conn_status(DISCONNECTED)
if self.frm_settings.frm_serial.status == NOPORTS:
- self.set_status(INTROTXTNOPORTS, ERRCOL)
+ self.status_label = (INTROTXTNOPORTS, ERRCOL)
# check for more recent version (if enabled)
if self.configuration.get("checkforupdate_b") and configerr == "":
@@ -240,8 +246,7 @@ def __init__(self, master, **kwargs): # pylint: disable=too-many-statements
# display any deferred messages
if isinstance(self._deferredmsg, tuple):
- msg, col = self._deferredmsg
- self.set_status(msg, col)
+ self.status_label = self._deferredmsg
self._deferredmsg = None
def _body(self):
@@ -253,7 +258,7 @@ def _body(self):
self.menu = MenuBar(self)
self.__master.config(menu=self.menu)
- # instantiate widgets
+ # initialise widget state
for value in self.widget_state.state.values():
setattr(
self,
@@ -265,6 +270,11 @@ def _do_layout(self):
"""
Arrange widgets in main application frame, and set
widget visibility and menu label (show/hide).
+
+ NB: PyGPSClient generally favours 'grid' rather than 'pack'
+ layout management throughout:
+ - grid weight = 0 means fixed, non-expandable
+ - grid weight > 0 means expandable
"""
col = 0
@@ -277,15 +287,10 @@ def _do_layout(self):
name, col, row, maxcol, maxrow, men
)
- # ensure widgets expand to size of container (needed
- # when not using 'pack' grid management)
- # weight = 0 means fixed, non-expandable
- # weight > 0 means expandable
for col in range(MAXCOLSPAN + 1):
self.__master.grid_columnconfigure(col, weight=0)
for row in range(MAXROWSPAN + 2):
self.__master.grid_rowconfigure(row, weight=0)
- # print(f"{maxcol=} {maxrow=}")
for col in range(maxcol):
self.__master.grid_columnconfigure(col, weight=5)
for row in range(1, maxrow + 1):
@@ -297,9 +302,9 @@ def _widget_grid(
"""
Arrange widgets and update menu label (show/hide).
- Widgets with explicit COL settings will be placed in fixed
- positions; widgets with no COL setting will be arranged
- dynamically.
+ Widgets with explicit COL(umn) settings will be placed in fixed
+ positions; widgets with no COL(umn) setting will be arranged
+ dynamically (left to right, top to bottom).
:param str name: name of widget
:param int col: col
@@ -330,7 +335,7 @@ def _widget_grid(
rowspan=rowspan,
padx=2,
pady=2,
- sticky=wdg.get(STICKY, (N, S, W, E)),
+ sticky=wdg.get(STICKY, NSEW),
)
lbl = HIDE
if dynamic:
@@ -350,7 +355,7 @@ def _widget_grid(
men += 1
# force widget to rescale
- frm.event_generate("")
+ # frm.event_generate("")
return col, row, maxcol, maxrow, men
@@ -370,7 +375,7 @@ def widget_toggle(self, name: str):
def widget_enable_messages(self, name: str):
"""
- Enable any NMEA, UBX or RTCM messages required by widget.
+ Enable any GNSS messages required by widget.
:param str name: widget name
"""
@@ -392,7 +397,7 @@ def widget_reset(self):
def reset_gnssstatus(self):
"""
- Reset gnss_status dict e.g. after reconnecting.
+ Reset gnss_status dictionary e.g. after reconnecting.
"""
self.gnss_status = GNSSStatus()
@@ -423,44 +428,9 @@ def _set_default_fonts(self):
self.font_md2 = font.Font(size=14)
self.font_lg = font.Font(size=18)
- def set_connection(self, message, color=OKCOL):
- """
- Sets connection description in status bar.
-
- :param str message: message to be displayed in connection label
- :param str color: rgb color string
-
- """
-
- if hasattr(self, "frm_status"):
- self.frm_status.set_connection(message, color=color)
-
- def set_status(self, message, color=OKCOL):
- """
- Sets text of status bar, or defer if frm_status not yet instantiated.
-
- :param str message: message to be displayed in status label
- :param str color: rgb color string
- """
-
- def priority(col):
- return STATUSPRIORITY.get(col, 0)
-
- if hasattr(self, "frm_status"):
- color = INFOCOL if color == "blue" else color
- self.frm_status.set_status(message, color)
- self.update_idletasks()
- else: # defer message until frm_status is instantiated
- if isinstance(self._deferredmsg, tuple):
- defpty = priority(self._deferredmsg[1])
- else:
- defpty = 0
- if priority(color) > defpty:
- self._deferredmsg = (message, color)
-
def set_event(self, evt: str):
"""
- Generate event
+ Generate master event.
:param str evt: event type string
"""
@@ -474,9 +444,9 @@ def load_config(self):
# Warn if Streaming, NTRIP or SPARTN clients are running
if self.conn_status == DISCONNECTED and self.rtk_conn_status == DISCONNECTED:
- self.set_status("", OKCOL)
+ self.status_label = ("", OKCOL)
else:
- self.set_status(DLGSTOPRTK, ERRCOL)
+ self.status_label = (DLGSTOPRTK, ERRCOL)
return
filename, err = self.configuration.loadfile()
@@ -491,13 +461,13 @@ def load_config(self):
frm.reset()
self._do_layout()
if self._nowidgets:
- self.set_status(NOWDGSWARN.format(filename), ERRCOL)
+ self.status_label = (NOWDGSWARN.format(filename), ERRCOL)
else:
- self.set_status(LOADCONFIGOK.format(filename), OKCOL)
+ self.status_label = (LOADCONFIGOK.format(filename), OKCOL)
elif err == "cancelled": # user cancelled
return
else: # config error
- self.set_status(LOADCONFIGBAD.format(filename), ERRCOL)
+ self.status_label = (LOADCONFIGBAD.format(filename), ERRCOL)
def save_config(self):
"""
@@ -506,9 +476,9 @@ def save_config(self):
err = self.configuration.savefile()
if err == "":
- self.set_status(SAVECONFIGOK, OKCOL)
+ self.status_label = (SAVECONFIGOK, OKCOL)
else: # save failed
- self.set_status(SAVECONFIGBAD.format(err), ERRCOL)
+ self.status_label = (SAVECONFIGBAD.format(err), ERRCOL)
def update_widgets(self):
"""
@@ -528,45 +498,32 @@ def update_widgets(self):
if self._nowidgets:
self.widget_state.state["Status"][VISIBLE] = "true"
except KeyError as err:
- self.set_status(f"{CONFIGERR} - {err}", ERRCOL)
-
- def start_dialog(self, dlg: str):
- """
- Start a threaded dialog task if the dialog is not already open.
+ self.status_label = (f"{CONFIGERR} - {err}", ERRCOL)
- :param str dlg: name of dialog
- """
-
- if self.dialog_state.state[dlg][THD] is None:
- self.dialog_state.state[dlg][THD] = Thread(
- target=self._dialog_thread, args=(dlg,), daemon=False
- )
- self.dialog_state.state[dlg][THD].start()
-
- def _dialog_thread(self, dlg: str):
+ def _refresh_widgets(self):
"""
- THREADED PROCESS
-
- Dialog thread.
-
- :param str dlg: name of dialog
+ Refresh visible widgets.
"""
- config = (
- self.configuration.settings if self.dialog_state.state[dlg][CFG] else {}
- )
- cls = self.dialog_state.state[dlg][CLASS]
- self.dialog_state.state[dlg][DLG] = cls(self, saved_config=config)
+ for wdg, wdgdata in self.widget_state.state.items():
+ frm = getattr(self, wdgdata[FRAME])
+ if hasattr(frm, "update_frame") and wdgdata[VISIBLE]:
+ if wdg == WDGCONSOLE:
+ frm.update_frame(self.consoledata)
+ self.consoledata = []
+ else:
+ frm.update_frame()
- def stop_dialog(self, dlg: str):
+ def start_dialog(self, dlg: str):
"""
- Register dialog as closed.
+ Open a top level dialog if the dialog is not already open.
:param str dlg: name of dialog
"""
- self.dialog_state.state[dlg][THD] = None
- self.dialog_state.state[dlg][DLG] = None
+ if self.dialog_state.state[dlg][DLG] is None:
+ cls = self.dialog_state.state[dlg][CLASS]
+ self.dialog_state.state[dlg][DLG] = cls(self)
def dialog(self, dlg: str) -> Toplevel:
"""
@@ -632,7 +589,7 @@ def _sockserver_thread(
socketqueue: Queue,
):
"""
- THREADED
+ THREADED PROCESS
Socket Server thread.
:param int ntripmode: 0 = open socket server, 1 = NTRIP server
@@ -657,11 +614,12 @@ def _sockserver_thread(
) as self._socket_server:
self._socket_server.serve_forever()
except OSError as err:
- self.set_status(f"Error starting socket server {err}", ERRCOL)
+ self.status_label = (f"Error starting socket server {err}", ERRCOL)
def update_clients(self, clients: int):
"""
Update number of connected clients in settings panel.
+ Called by pygnssutils.socket_server.
:param int clients: no of connected clients
"""
@@ -697,12 +655,12 @@ def on_killswitch(self, *args, **kwargs): # pylint: disable=unused-argument
for dlg in self.dialog_state.state:
if self.dialog(dlg) is not None:
self.dialog(dlg).destroy()
- self.stop_dialog(dlg)
+ # self.stop_dialog(dlg)
self.conn_status = DISCONNECTED
self.rtk_conn_status = DISCONNECTED
except Exception as err: # pylint: disable=broad-exception-caught
self.logger.error(err)
- self.set_status(KILLSWITCH, ERRCOL)
+ self.status_label = (KILLSWITCH, ERRCOL)
self.logger.debug(KILLSWITCH)
def on_gnss_read(self, event): # pylint: disable=unused-argument
@@ -737,7 +695,7 @@ def on_gnss_eof(self, event): # pylint: disable=unused-argument
)
self._refresh_widgets()
self.conn_status = DISCONNECTED
- self.set_status(ENDOFFILE, ERRCOL)
+ self.status_label = (ENDOFFILE, ERRCOL)
def on_gnss_timeout(self, event): # pylint: disable=unused-argument
"""
@@ -752,7 +710,7 @@ def on_gnss_timeout(self, event): # pylint: disable=unused-argument
)
self._refresh_widgets()
self.conn_status = DISCONNECTED
- self.set_status(INACTIVE_TIMEOUT, ERRCOL)
+ self.status_label = (INACTIVE_TIMEOUT, ERRCOL)
def on_stream_error(self, event): # pylint: disable=unused-argument
"""
@@ -794,7 +752,7 @@ def on_ntrip_read(self, event): # pylint: disable=unused-argument
except Empty:
pass
except (SerialException, SerialTimeoutException) as err:
- self.set_status(f"Error sending to device {err}", ERRCOL)
+ self.status_label = (f"Error sending to device {err}", ERRCOL)
def on_spartn_read(self, event): # pylint: disable=unused-argument
"""
@@ -824,14 +782,14 @@ def on_spartn_read(self, event): # pylint: disable=unused-argument
except Empty:
pass
except (SerialException, SerialTimeoutException) as err:
- self.set_status(f"Error sending to device {err}", ERRCOL)
+ self.status_label = (f"Error sending to device {err}", ERRCOL)
- def update_ntrip_status(self, status: bool, msgt: tuple = None):
+ def update_ntrip_status(self, status: bool, msgt: tuple | NoneType = None):
"""
Update NTRIP configuration dialog connection status.
:param bool status: connected to NTRIP server yes/no
- :param tuple msgt: tuple of (message, color)
+ :param tuple | None msgt: tuple of (message, color) or None
"""
if self.dialog(DLGTNTRIP) is not None:
@@ -839,7 +797,9 @@ def update_ntrip_status(self, status: bool, msgt: tuple = None):
def get_coordinates(self) -> dict:
"""
- Get current coordinates and fix data.
+ Supply current coordinates and fix data to any widget
+ that requests it (mirrors NMEA GGA format).
+ Called by pygnssutils.ntrip_client.
:return: dict of coords and fix data
:rtype: dict
@@ -865,13 +825,16 @@ def get_coordinates(self) -> dict:
def process_data(self, raw_data: bytes, parsed_data: object, marker: str = ""):
"""
- Update the various GUI widgets, GPX track and log file.
+ THIS IS THE MAIN GNSS DATA PROCESSING LOOP
+
+ Update the various GUI widgets, data & gpx logs and database.
:param bytes raw_data: raw message data
:param object parsed data: NMEAMessage, UBXMessage or RTCMMessage
:param str marker: string prepended to console entries e.g. "NTRIP>>"
"""
+ start = process_time_ns()
# self.logger.debug(f"data received {parsed_data.identity}")
msgprot = 0
protfilter = self.protocol_mask
@@ -912,7 +875,7 @@ def process_data(self, raw_data: bytes, parsed_data: object, marker: str = ""):
elif msgprot == MQTT_PROTOCOL:
pass
- # update chart data if chart is visible
+ # update chart plot if chart is visible
if self.widget_state.state[WDGCHART][VISIBLE]:
getattr(self, self.widget_state.state[WDGCHART][FRAME]).update_data(
parsed_data
@@ -943,12 +906,13 @@ def process_data(self, raw_data: bytes, parsed_data: object, marker: str = ""):
self.file_handler.write_logfile(raw_data, parsed_data)
self.update_idletasks()
+ self.processtime = process_time_ns() - start
def send_to_device(self, data: object):
"""
Send raw data to connected device.
- :param object data: raw GNSS data (NMEA, UBX, ASCII, RTCM3, SPARTN)
+ :param object data: raw GNSS data (NMEA, UBX, TTY, RTCM3, SPARTN)
"""
self.logger.debug(f"Sending message {data}")
@@ -959,20 +923,6 @@ def send_to_device(self, data: object):
):
self.gnss_outqueue.put(data)
- def _refresh_widgets(self):
- """
- Refresh visible widgets.
- """
-
- for wdg, wdgdata in self.widget_state.state.items():
- frm = getattr(self, wdgdata[FRAME])
- if hasattr(frm, "update_frame") and wdgdata[VISIBLE]:
- if wdg == WDGCONSOLE:
- frm.update_frame(self.consoledata)
- self.consoledata = []
- else:
- frm.update_frame()
-
def _check_update(self):
"""
Check for updated version.
@@ -980,7 +930,7 @@ def _check_update(self):
latest = check_latest(TITLE)
if latest not in (VERSION, "N/A"):
- self.set_status(f"{VERCHECK} {latest}", ERRCOL)
+ self.status_label = (f"{VERCHECK} {latest}", ERRCOL)
def poll_version(self, protocol: int):
"""
@@ -999,14 +949,10 @@ def poll_version(self, protocol: int):
if isinstance(msg, (UBXMessage, NMEAMessage)):
self.send_to_device(msg.serialize())
- self.set_status(
- f"{msg.identity} POLL message sent",
- )
+ self.status_label = (f"{msg.identity} POLL message sent", INFOCOL)
elif isinstance(msg, bytes):
self.send_to_device(msg)
- self.set_status(
- "Setup POLL message sent",
- )
+ self.status_label = ("Setup POLL message sent", INFOCOL)
@property
def appmaster(self) -> Tk:
@@ -1019,6 +965,85 @@ def appmaster(self) -> Tk:
return self.__master
+ @property
+ def conn_label(self) -> Label:
+ """
+ Getter for connection_label.
+
+ :return: status label
+ :rtype: Label
+ """
+
+ return self.frm_status.lbl_connection
+
+ @conn_label.setter
+ def conn_label(self, connection: str | tuple[str, str]):
+ """
+ Sets connection description in status bar.
+
+ :param str | tuple connection: (connection, color)
+ """
+
+ if isinstance(connection, tuple):
+ connection, color = connection
+ else:
+ color = INFOCOL
+
+ # truncate very long connection description
+ if len(connection) > 100:
+ connection = "..." + connection[-100:]
+
+ if hasattr(self, "frm_status"):
+ self.conn_label.after(
+ 0, self.conn_label.config, {"text": connection, "fg": color}
+ )
+ self.update_idletasks()
+
+ @property
+ def status_label(self) -> Label:
+ """
+ Getter for status_label.
+
+ :return: status label
+ :rtype: Label
+ """
+
+ return self.frm_status.lbl_status
+
+ @status_label.setter
+ def status_label(self, message: str | tuple[str, str]):
+ """
+ Sets status message, or defers if frm_status not yet instantiated.
+
+ :param str | tuple message: (message, color)
+ """
+
+ def priority(col):
+ return STATUSPRIORITY.get(col, 0)
+
+ if isinstance(message, tuple):
+ message, color = message
+ else:
+ color = INFOCOL
+
+ # truncate very long messages
+ if len(message) > 200:
+ message = "..." + message[-200:]
+
+ if hasattr(self, "frm_status"):
+ color = INFOCOL if color == "blue" else color
+ self.status_label.after(
+ 0, self.status_label.config, {"text": message, "fg": color}
+ )
+ self.update_idletasks()
+ else: # defer message until frm_status is instantiated
+ if isinstance(self._deferredmsg, tuple):
+ defpty = priority(self._deferredmsg[1])
+ else:
+ defpty = 0
+ if priority(color) > defpty:
+ self._deferredmsg = (message, color)
+
@property
def conn_status(self) -> int:
"""
@@ -1042,12 +1067,12 @@ def conn_status(self, status: int):
self.frm_banner.update_conn_status(status)
self.frm_settings.enable_controls(status)
if status == DISCONNECTED:
- self.set_connection(NOTCONN)
+ self.conn_label = (NOTCONN, INFOCOL)
@property
def rtk_conn_status(self) -> int:
"""
- Getter for SPARTN connection status.
+ Getter for RTK connection status.
:return: connection status
:rtype: int
@@ -1058,7 +1083,7 @@ def rtk_conn_status(self) -> int:
@rtk_conn_status.setter
def rtk_conn_status(self, status: int):
"""
- Setter for SPARTN connection status.
+ Setter for RTK connection status.
:param int status: connection status
"""
@@ -1089,21 +1114,24 @@ def protocol_mask(self) -> int:
return mask
@property
- def db_enabled(self) -> bool:
+ def db_enabled(self) -> int | str:
"""
Getter for database enabled status.
- :return: database enabled status
- :rtype: bool
+ :return: database enabled status or err code
+ :rtype: int | str
"""
return self._db_enabled
def do_app_update(self, updates: list) -> int:
"""
- Update outdated application modules to latest versions.
+ Update outdated application packages to latest versions.
+
+ NB: Some platforms (e.g. Homebrew-installed Python environments)
+ may block Python subprocess calls ('run') on security grounds.
- :param list updates: list of modules to be updated
+ :param list updates: list of packages to be updated
:return: return code 0 = error, 1 = OK
:rtype: int
"""
diff --git a/src/pygpsclient/banner_frame.py b/src/pygpsclient/banner_frame.py
index 8628e5ce..b1945a59 100644
--- a/src/pygpsclient/banner_frame.py
+++ b/src/pygpsclient/banner_frame.py
@@ -12,7 +12,8 @@
:license: BSD 3-Clause
"""
-from tkinter import NW, SUNKEN, Button, E, Frame, Label, N, W
+from time import time
+from tkinter import NE, NW, SUNKEN, Button, E, Frame, Label, N, W
from PIL import Image, ImageTk
from pynmeagps.nmeahelpers import latlon2dmm, latlon2dms, llh2ecef
@@ -46,7 +47,15 @@
UIK,
UMK,
)
-from pygpsclient.helpers import dop2str, m2ft, ms2kmph, ms2knots, ms2mph, scale_font
+from pygpsclient.helpers import (
+ dop2str,
+ m2ft,
+ ms2kmph,
+ ms2knots,
+ ms2mph,
+ scale_font,
+ unused_sats,
+)
from pygpsclient.strings import NA
DGPSYES = "YES"
@@ -202,6 +211,9 @@ def _body(self):
self._lbl_trk = Label(
self._frm_advanced, bg=self._bgcol, fg="deepskyblue", width=8, anchor=W
)
+ self._lbl_benchmark = Label(
+ self._frm_advanced, text="", bg=self._bgcol, fg="grey", width=15, anchor=E
+ )
self._lbl_siv = Label(
self._frm_advanced2, bg=self._bgcol, fg="yellow", width=2, anchor=W
)
@@ -230,7 +242,7 @@ def _body(self):
width=2,
)
self._lbl_diffstat = Label(
- self._frm_advanced2, bg=self._bgcol, fg="hotpink", width=20, anchor=W
+ self._frm_advanced2, bg=self._bgcol, fg="hotpink", width=25, anchor=W
)
def _do_layout(self):
@@ -258,6 +270,7 @@ def _do_layout(self):
self._lbl_spd.grid(column=3, row=0, pady=0, padx=0, sticky=W)
self._lbl_ltrk.grid(column=4, row=0, pady=0, padx=0, sticky=W)
self._lbl_trk.grid(column=5, row=0, pady=0, padx=0, sticky=W)
+ self._lbl_benchmark.grid(column=6, row=0, pady=0, padx=0, sticky=E)
self._lbl_lsiv.grid(column=0, row=0, pady=0, padx=0, sticky=W)
self._lbl_siv.grid(column=1, row=0, pady=0, padx=0, sticky=W)
self._lbl_lsip.grid(column=2, row=0, pady=0, padx=0, sticky=W)
@@ -272,7 +285,7 @@ def _do_layout(self):
self._lbl_diffcorr.grid(column=11, row=0, pady=0, padx=0, sticky=W)
self._lbl_diffstat.grid(column=12, row=0, pady=0, padx=0, sticky=W)
- self._btn_toggle.grid(column=0, row=0, padx=0, pady=0, sticky=(N, E))
+ self._btn_toggle.grid(column=0, row=0, padx=0, pady=0, sticky=NE)
self._toggle_advanced()
@@ -282,10 +295,10 @@ def _toggle_advanced(self):
"""
self._frm_connect.grid(
- column=0, row=0, rowspan=2, pady=3, ipadx=3, ipady=3, sticky=(N, W)
+ column=0, row=0, rowspan=2, pady=3, ipadx=3, ipady=3, sticky=NW
)
self._frm_basic.grid(column=1, row=0, pady=2, sticky=W)
- self._frm_toggle.grid(column=5, row=0, rowspan=2, pady=2, sticky=(N, E))
+ self._frm_toggle.grid(column=5, row=0, rowspan=2, pady=2, sticky=NE)
self._show_advanced = not self._show_advanced
if self._show_advanced:
self._frm_advanced.grid(column=1, row=1, pady=2, sticky=W)
@@ -377,6 +390,13 @@ def _update_time(self):
else:
self._lbl_time.config(text=f"{tim:%H:%M:%S.%f}")
+ # display run time in s and process time in µs
+ if self.__app.configuration.get("version_s") == "BENCHMARK":
+ tim = time()
+ self._lbl_benchmark.config(
+ text=f"{tim-self.__app.starttime:.0f} s {self.__app.processtime/1000:.0f} µs"
+ )
+
def _update_pos(self, pos_format, units):
"""
Update position.
@@ -470,11 +490,19 @@ def _update_fix(self):
def _update_siv(self):
"""
- Update siv and sip
+ Update siv and sip.
+
+ Exclude unused sats (cno = 0) if show_unused is not set.
"""
+ siv = self.__app.gnss_status.siv
+ siv = (
+ siv
+ if self.__app.configuration.get("unusedsat_b")
+ else siv - unused_sats(self.__app.gnss_status.gsv_data)
+ )
try:
- self._lbl_siv.config(text=f"{self.__app.gnss_status.siv:02d}")
+ self._lbl_siv.config(text=f"{siv:02d}")
self._lbl_sip.config(text=f"{self.__app.gnss_status.sip:02d}")
except (TypeError, ValueError):
self._lbl_siv.config(text=NA)
@@ -583,6 +611,7 @@ def _set_fontsize(self):
self._lbl_hae,
self._lbl_spd,
self._lbl_trk,
+ self._lbl_benchmark,
self._lbl_pdop,
self._lbl_fix,
self._lbl_sip,
diff --git a/src/pygpsclient/canvas_map.py b/src/pygpsclient/canvas_map.py
index 869b3318..a4feeaca 100644
--- a/src/pygpsclient/canvas_map.py
+++ b/src/pygpsclient/canvas_map.py
@@ -62,7 +62,7 @@
point_in_bounds,
scale_font,
)
-from pygpsclient.mapquest import (
+from pygpsclient.mapquest_handler import (
HYB,
MAP,
MAPQTIMEOUT,
diff --git a/src/pygpsclient/chart_frame.py b/src/pygpsclient/chart_frame.py
index 051a104a..07f2bc36 100644
--- a/src/pygpsclient/chart_frame.py
+++ b/src/pygpsclient/chart_frame.py
@@ -17,17 +17,15 @@
from random import choice
from time import time
from tkinter import (
+ EW,
NORMAL,
- E,
+ NSEW,
Entry,
Frame,
Label,
- N,
- S,
Spinbox,
StringVar,
TclError,
- W,
font,
)
@@ -299,22 +297,22 @@ def _do_layout(self):
Position widgets in frame.
"""
- self._canvas.grid(column=0, row=0, columnspan=6, sticky=(N, S, E, W))
- self._lbl_id.grid(column=0, row=1, sticky=(W, E))
- self._lbl_name.grid(column=1, row=1, sticky=(W, E))
- self._lbl_scale.grid(column=2, row=1, sticky=(W, E))
- self._lbl_miny.grid(column=3, row=1, sticky=(W, E))
- self._lbl_maxy.grid(column=4, row=1, sticky=(W, E))
+ self._canvas.grid(column=0, row=0, columnspan=6, sticky=NSEW)
+ self._lbl_id.grid(column=0, row=1, sticky=EW)
+ self._lbl_name.grid(column=1, row=1, sticky=EW)
+ self._lbl_scale.grid(column=2, row=1, sticky=EW)
+ self._lbl_miny.grid(column=3, row=1, sticky=EW)
+ self._lbl_maxy.grid(column=4, row=1, sticky=EW)
for chn in range(self._num_chans):
- self._ent_id[chn].grid(column=0, row=2 + chn, sticky=(W, E))
- self._ent_name[chn].grid(column=1, row=2 + chn, sticky=(W, E))
- self._spn_scale[chn].grid(column=2, row=2 + chn, sticky=(W, E))
- self._spn_miny[chn].grid(column=3, row=2 + chn, sticky=(W, E))
- self._spn_maxy[chn].grid(column=4, row=2 + chn, sticky=(W, E))
- self._lbl_timrange.grid(column=5, row=1, sticky=(W, E))
- self._spn_timrange.grid(column=5, row=2, sticky=(W, E))
- self._lbl_maxpoints.grid(column=5, row=3, sticky=(W, E))
- self._spn_maxpoints.grid(column=5, row=4, sticky=(W, E))
+ self._ent_id[chn].grid(column=0, row=2 + chn, sticky=EW)
+ self._ent_name[chn].grid(column=1, row=2 + chn, sticky=EW)
+ self._spn_scale[chn].grid(column=2, row=2 + chn, sticky=EW)
+ self._spn_miny[chn].grid(column=3, row=2 + chn, sticky=EW)
+ self._spn_maxy[chn].grid(column=4, row=2 + chn, sticky=EW)
+ self._lbl_timrange.grid(column=5, row=1, sticky=EW)
+ self._spn_timrange.grid(column=5, row=2, sticky=EW)
+ self._lbl_maxpoints.grid(column=5, row=3, sticky=EW)
+ self._spn_maxpoints.grid(column=5, row=4, sticky=EW)
def _attach_events(self):
"""
@@ -425,7 +423,9 @@ def update_data(self, parsed_data: object):
continue
# wildcards *+-, sum, max or min of group of values
- if name[-1] in ("*", "+", "-"):
+ if name == "processtime":
+ val = self.__app.processtime / 1000 # microseconds
+ elif name[-1] in ("*", "+", "-"):
vals = []
for attr in parsed_data.__dict__:
if name[:-1] in attr and name[0] != "_":
@@ -593,6 +593,7 @@ 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/configuration.py b/src/pygpsclient/configuration.py
index 0a951feb..8372c689 100644
--- a/src/pygpsclient/configuration.py
+++ b/src/pygpsclient/configuration.py
@@ -10,8 +10,11 @@
:license: BSD 3-Clause
"""
+# pylint: disable=logging-format-interpolation
+
import logging
from os import getenv
+from types import NoneType
from pyubx2 import GET
from serial import PARITY_NONE
@@ -27,6 +30,7 @@
FORMAT_BINARY,
FORMAT_PARSED,
GUI_UPDATE_INTERVAL,
+ MAXLOGSIZE,
MIN_GUI_UPDATE_INTERVAL,
MQTTIPMODE,
OKCOL,
@@ -47,13 +51,14 @@
ZED_F9,
)
from pygpsclient.init_presets import INIT_PRESETS
-from pygpsclient.mapquest import MAP_UPDATE_INTERVAL
+from pygpsclient.mapquest_handler import MAP_UPDATE_INTERVAL
from pygpsclient.spartn_lband_frame import D9S_PP_EU as D9S_PP
from pygpsclient.strings import (
LOADCONFIGBAD,
LOADCONFIGNK,
LOADCONFIGNONE,
LOADCONFIGOK,
+ LOADCONFIGRESAVE,
)
from pygpsclient.widget_state import VISIBLE
@@ -128,6 +133,7 @@ def __init__(self, app):
"datalog_b": 0,
"logformat_s": FORMAT_BINARY,
"logpath_s": "",
+ "logsize_n": MAXLOGSIZE,
"recordtrack_b": 0,
"trackpath_s": "",
"database_b": 0,
@@ -235,9 +241,8 @@ def __init__(self, app):
"scatterlon_f": 0.0,
},
"imusettings_d": {
- "source_s": "ESF-ALG",
"range_n": 180,
- "option_s": "N/A",
+ "option_s": "N/A", # reserved for future use
},
"chartsettings_d": {
"numchn_n": 4,
@@ -251,11 +256,11 @@ def __init__(self, app):
"colortags_l": [],
}
- def loadfile(self, filename: str = None) -> tuple:
+ def loadfile(self, filename: str | NoneType = None) -> tuple:
"""
Load configuration from json file.
- :param str filename: config file name
+ :param str | NoneType filename: config file name
:return: tuple of filename and err message (or "" if OK)
:rtype: tuple
"""
@@ -263,18 +268,20 @@ def loadfile(self, filename: str = None) -> tuple:
fname, config, err = self.__app.file_handler.load_config(filename)
key = ""
val = 0
+ resave = False
if err == "": # load succeeded
- try:
- for key, val in config.items():
- key = key.replace("mgtt", "mqtt") # tolerate "mgtt" typo
- if key == "protocol_n": # redundant, ignore
- continue
- if key == "guiupdateinterval_f": # disallow excessive value
- val = max(MIN_GUI_UPDATE_INTERVAL, val)
+ for key, val in config.items():
+ if key == "version_s" and val != version:
+ resave = True
+ key = key.replace("mgtt", "mqtt") # tolerate "mgtt" typo
+ if key == "guiupdateinterval_f": # disallow excessive value
+ val = max(MIN_GUI_UPDATE_INTERVAL, val)
+ try:
self.set(key, val)
- # self.__app.set_status(LOADCONFIGOK.format(fname), OKCOL)
- except KeyError: # unrecognised setting
- err = LOADCONFIGNK.format(key, val)
+ except KeyError: # ignore unrecognised setting
+ self.logger.info(LOADCONFIGNK.format(key, val))
+ resave = True
+ continue
else:
if "No such file or directory" in err:
err = LOADCONFIGNONE.format(fname)
@@ -282,21 +289,23 @@ def loadfile(self, filename: str = None) -> tuple:
err = LOADCONFIGBAD.format(fname, err)
if err == "": # config valid
- self.__app.set_status(LOADCONFIGOK.format(fname), OKCOL)
+ rs = LOADCONFIGRESAVE if resave else ""
+ self.__app.status_label = (LOADCONFIGOK.format(fname, rs), OKCOL)
else:
- self.__app.set_status(err, ERRCOL)
+ self.__app.status_label = (err, ERRCOL)
return fname, err
- def savefile(self, filename: str = None) -> str:
+ def savefile(self, filename: str | NoneType = None) -> str:
"""
Save configuration to json file.
- :param str filename: config file name
+ :param str | NoneType filename: config file name
:return: error code, or "" if OK
:rtype: str
"""
+ self.set("version_s", version)
return self.__app.file_handler.save_config(self.settings, filename)
def loadcli(self, **kwargs):
diff --git a/src/pygpsclient/console_frame.py b/src/pygpsclient/console_frame.py
index e7c2319c..55db8fca 100644
--- a/src/pygpsclient/console_frame.py
+++ b/src/pygpsclient/console_frame.py
@@ -17,7 +17,18 @@
:license: BSD 3-Clause
"""
-from tkinter import END, HORIZONTAL, NONE, VERTICAL, E, Frame, N, S, Scrollbar, Text, W
+from tkinter import (
+ END,
+ EW,
+ HORIZONTAL,
+ NONE,
+ NS,
+ NSEW,
+ VERTICAL,
+ Frame,
+ Scrollbar,
+ Text,
+)
from pyubx2 import hextable
@@ -109,9 +120,9 @@ def _do_layout(self):
Set position of widgets in frame
"""
- self.txt_console.grid(column=0, row=0, pady=1, padx=1, sticky=(N, S, E, W))
- self.sblogv.grid(column=1, row=0, sticky=(N, S, E))
- self.sblogh.grid(column=0, row=1, sticky=(S, E, W))
+ self.txt_console.grid(column=0, row=0, pady=1, padx=1, sticky=NSEW)
+ self.sblogv.grid(column=1, row=0, sticky=NS)
+ self.sblogh.grid(column=0, row=1, sticky=EW)
def _attach_events(self):
"""
@@ -217,7 +228,7 @@ def _on_halt(self, event): # pylint: disable=unused-argument
"""
self.__app.stream_handler.stop()
- self.__app.set_status(HALTTAGWARN.format(self._halt), ERRCOL)
+ self.__app.status_label = (HALTTAGWARN.format(self._halt), ERRCOL)
self.__app.conn_status = DISCONNECTED
def _on_clipboard(self, event): # pylint: disable=unused-argument
diff --git a/src/pygpsclient/dialog_state.py b/src/pygpsclient/dialog_state.py
index c7b0e600..9da3deb9 100644
--- a/src/pygpsclient/dialog_state.py
+++ b/src/pygpsclient/dialog_state.py
@@ -7,7 +7,6 @@
CLASS = name of dialog class
THD = instance of thread
DLG = instance of dialog frame
-CFG = whether to pass configuration data to dialog
RESIZE = whether dialog is resizeable
Created on 16 Aug 2023
@@ -18,7 +17,7 @@
"""
from pygpsclient.about_dialog import AboutDialog
-from pygpsclient.globals import CFG, CLASS, RESIZE, THD
+from pygpsclient.globals import CLASS, RESIZE
from pygpsclient.gpx_dialog import GPXViewerDialog
from pygpsclient.importmap_dialog import ImportMapDialog
from pygpsclient.nmea_config_dialog import NMEAConfigDialog
@@ -52,58 +51,42 @@ def __init__(self):
self.state = {
DLGTABOUT: {
CLASS: AboutDialog,
- THD: None,
DLG: None,
- CFG: False,
RESIZE: False,
},
DLGTUBX: {
CLASS: UBXConfigDialog,
- THD: None,
DLG: None,
- CFG: True,
RESIZE: False,
},
DLGTNMEA: {
CLASS: NMEAConfigDialog,
- THD: None,
DLG: None,
- CFG: True,
RESIZE: False,
},
DLGTNTRIP: {
CLASS: NTRIPConfigDialog,
- THD: None,
DLG: None,
- CFG: True,
RESIZE: False,
},
DLGTSPARTN: {
CLASS: SPARTNConfigDialog,
- THD: None,
DLG: None,
- CFG: True,
RESIZE: False,
},
DLGTGPX: {
CLASS: GPXViewerDialog,
- THD: None,
DLG: None,
- CFG: True,
RESIZE: True,
},
DLGTIMPORTMAP: {
CLASS: ImportMapDialog,
- THD: None,
DLG: None,
- CFG: True,
RESIZE: True,
},
DLGTTTY: {
CLASS: TTYPresetDialog,
- THD: None,
DLG: None,
- CFG: True,
RESIZE: True,
},
# add any new dialogs here
diff --git a/src/pygpsclient/dynamic_config_frame.py b/src/pygpsclient/dynamic_config_frame.py
index c7c54e48..a8f80f02 100644
--- a/src/pygpsclient/dynamic_config_frame.py
+++ b/src/pygpsclient/dynamic_config_frame.py
@@ -21,7 +21,9 @@
from tkinter import (
ALL,
END,
+ EW,
LEFT,
+ NSEW,
NW,
VERTICAL,
Button,
@@ -244,24 +246,24 @@ def _do_layout(self):
Layout widgets.
"""
- self._lbl_cfg_dyn.grid(column=0, row=0, columnspan=4, padx=3, sticky=(W, E))
+ self._lbl_cfg_dyn.grid(column=0, row=0, columnspan=4, padx=3, sticky=EW)
self._lbx_cfg_cmd.grid(
- column=0, row=1, columnspan=2, rowspan=6, padx=3, pady=3, sticky=(W, E)
+ column=0, row=1, columnspan=2, rowspan=6, padx=3, pady=3, sticky=EW
)
self._scr_cfg_cmd.grid(column=1, row=1, rowspan=6, sticky=(N, S, E))
self._btn_send_command.grid(column=3, row=1, ipadx=3, ipady=3, sticky=W)
self._lbl_send_command.grid(column=3, row=2, ipadx=3, ipady=3, sticky=W)
self._btn_refresh.grid(column=3, row=3, ipadx=3, ipady=3, sticky=W)
- self._lbl_command.grid(column=0, row=7, columnspan=4, padx=3, sticky=(W, E))
+ self._lbl_command.grid(column=0, row=7, columnspan=4, padx=3, sticky=EW)
self._frm_container.grid(
- column=0, row=8, columnspan=4, rowspan=15, padx=3, sticky=(N, S, W, E)
+ column=0, row=8, columnspan=4, rowspan=15, padx=3, sticky=NSEW
)
self._can_container.grid(
- column=0, row=0, columnspan=3, rowspan=15, padx=3, sticky=(N, S, W, E)
+ column=0, row=0, columnspan=3, rowspan=15, padx=3, sticky=NSEW
)
self._scr_container_ver.grid(column=3, row=0, rowspan=15, sticky=(N, S, E))
self._scr_container_hor.grid(
- column=0, row=15, columnspan=4, rowspan=15, sticky=(W, E)
+ column=0, row=15, columnspan=4, rowspan=15, sticky=EW
)
(cols, rows) = self.grid_size()
@@ -324,13 +326,14 @@ def _on_select_cfg(self, *args, **kwargs): # pylint: disable=unused-argument
self._expected_response = None
idx = self._lbx_cfg_cmd.curselection()
- self._cfg_id = self._lbx_cfg_cmd.get(idx)[1:]
if self._protocol == NMEA:
+ self._cfg_id = self._lbx_cfg_cmd.get(idx)[1:]
cfgid = self._cfg_id.rsplit("_", 1)[0]
pde = NMEA_MSGIDS_PROP[cfgid].replace("Sets/Gets", "")
pdesc = f"P{cfgid} {pde}"
pdic = NMEA_PAYLOADS_SET_PROP[self._cfg_id]
else: # UBX
+ self._cfg_id = self._lbx_cfg_cmd.get(idx)
pdesc = self._cfg_id
pdic = UBX_PAYLOADS_SET[self._cfg_id]
self._lbl_command.config(text=f"{pdesc}")
@@ -345,7 +348,7 @@ def _on_set_cfg(self, *args, **kwargs): # pylint: disable=unused-argument
"""
if self._cfg_id in ("", None):
- self.__container.set_status("Select command", ERRCOL)
+ self.__container.status_label = ("Select command", ERRCOL)
return
nam = ""
@@ -377,16 +380,14 @@ def _on_set_cfg(self, *args, **kwargs): # pylint: disable=unused-argument
# send message, update status and await response
self.__container.send_command(msg)
self._lbl_send_command.config(image=self._img_pending)
- self.__container.set_status(
- f"P{self._cfg_id} SET message sent",
- )
+ self.__container.status_label = f"P{self._cfg_id} SET message sent"
for msgid in pendcfg:
self.__container.set_pending(msgid, penddlg)
self._expected_response = SET
except ValueError as err:
self.logger.debug(traceback.format_exc())
- self.__container.set_status(
+ self.__container.status_label = (
f"INVALID! {nam}, {att}: {err}",
ERRCOL,
)
@@ -401,7 +402,7 @@ def _do_poll_cfg(self, *args, **kwargs): # pylint: disable=unused-argument
"""
if self._cfg_id in ("", None):
- self.__container.set_status("Select command", ERRCOL)
+ self.__container.status_label = ("Select command", ERRCOL)
return
msg = penddlg = pendcfg = None
@@ -423,17 +424,16 @@ def _do_poll_cfg(self, *args, **kwargs): # pylint: disable=unused-argument
penddlg = UBX_CFGOTHER
pendcfg = (msg.identity, NAK)
+ cp = "P" if self._protocol == NMEA else ""
if msg is not None:
self.__container.send_command(msg)
- self.__container.set_status(f"P{cfg_id} POLL message sent", INFOCOL)
+ self.__container.status_label = f"{cp}{cfg_id} POLL message sent"
self._lbl_send_command.config(image=self._img_pending)
for msgid in pendcfg:
self.__container.set_pending(msgid, penddlg)
self._expected_response = POLL
else: # CFG cannot be POLLed
- self.__container.set_status(
- f"P{cfg_id} No POLL available",
- )
+ self.__container.status_label = f"{cp}{cfg_id} No POLL available"
self._lbl_send_command.config(image=self._img_unknown)
def _do_poll_args(self, cfg_id: str) -> dict:
@@ -509,10 +509,13 @@ def update_status(self, msg: object):
self._update_widgets(msg)
if ok:
- self.__container.set_status(f"P{cfg_id} message acknowledged", OKCOL)
+ self.__container.status_label = (
+ f"{cfg_id} message acknowledged",
+ OKCOL,
+ )
self._lbl_send_command.config(image=self._img_confirmed)
else:
- self.__container.set_status(f"P{cfg_id} message rejected", ERRCOL)
+ self.__container.status_label = (f"{cfg_id} message rejected", ERRCOL)
self._lbl_send_command.config(image=self._img_warn)
self.update()
diff --git a/src/pygpsclient/file_handler.py b/src/pygpsclient/file_handler.py
index f2bcc514..3df6344b 100644
--- a/src/pygpsclient/file_handler.py
+++ b/src/pygpsclient/file_handler.py
@@ -21,7 +21,8 @@
import logging
from datetime import datetime, timedelta
from pathlib import Path
-from tkinter import filedialog
+from tkinter import Frame, Toplevel, filedialog
+from types import NoneType
from pyubx2 import hextable
@@ -37,7 +38,6 @@
GPX_NS,
GPX_TRACK_INTERVAL,
HOME,
- MAXLOGLINES,
XML_HDR,
)
from pygpsclient.helpers import set_filename
@@ -73,7 +73,7 @@ def __init__(self, app):
self._configpath = None
self._configfile = None
self._initdir = {}
- self._lines = 0
+ self._logsize = 0
self._last_track_update = datetime.fromordinal(1)
def __del__(self):
@@ -84,20 +84,27 @@ def __del__(self):
self.close_logfile()
self.close_trackfile()
- def open_file(self, mode: str, exts: tuple = DEFEXT) -> str:
+ def open_file(
+ self,
+ parent: Frame | Toplevel,
+ mode: str,
+ exts: tuple = DEFEXT,
+ ) -> str | NoneType:
"""
Generic routine to open specified file type.
+ :param Frame | Toplevel | NoneType: parent: parent window
:param str mode: type of file e.g. "config", "gpxtrack" etc.
:param tuple exts: tuple of file types ("description", "ext")
:return: fully qualified path to file, or None if user cancelled
- :rtype: str
+ :rtype: str | NoneType
"""
fil = filedialog.askopenfilename(
title=f"Open {mode.upper()} File",
initialdir=self._initdir.get(mode, HOME),
filetypes=exts,
+ parent=parent,
)
if fil in ((), ""):
return None # User cancelled
@@ -204,13 +211,13 @@ def save_config(self, config: dict, filename: Path = CONFIGFILE) -> str:
except (OSError, json.JSONDecodeError) as err:
return str(err)
- def set_logfile_path(self, initdir=HOME) -> Path:
+ def set_logfile_path(self, initdir=HOME) -> Path | NoneType:
"""
Set file path.
:param str initdir: initial directory (HOME)
:return: file path
- :rtype: str
+ :rtype: str | NoneType
"""
self._logpath = filedialog.askdirectory(
@@ -232,12 +239,12 @@ def open_logfile(self) -> int:
try:
self._logpath = self.__app.configuration.get("logpath_s")
- self._lines = 0
+ self._logsize = 0
_, self._logname = set_filename(self._logpath, "data", "log")
self._logfile = open(self._logname, "a+b")
return 1
except FileNotFoundError as err:
- self.__app.set_status(f"{err}", ERRCOL)
+ self.__app.status_label = (f"{err}", ERRCOL)
return 0
def write_logfile(self, raw_data, parsed_data):
@@ -252,6 +259,7 @@ def write_logfile(self, raw_data, parsed_data):
return
lfm = self.__app.configuration.get("logformat_s")
+ maxsize = self.__app.configuration.get("logsize_n")
data = []
if lfm in (FORMAT_PARSED, FORMAT_BOTH):
data.append(parsed_data)
@@ -268,11 +276,11 @@ def write_logfile(self, raw_data, parsed_data):
try:
self._logfile.write(datum)
self._logfile.flush()
- self._lines += 1
+ self._logsize += len(datum)
except ValueError:
pass
- if self._lines > MAXLOGLINES:
+ if self._logsize > maxsize:
self.close_logfile()
self.open_logfile()
@@ -318,7 +326,7 @@ def open_trackfile(self) -> int:
_, self._trackname = set_filename(self._trackpath, "track", "gpx")
self._trackfile = open(self._trackname, "a", encoding="utf-8")
except FileNotFoundError as err:
- self.__app.set_status(f"{err}", ERRCOL)
+ self.__app.status_label = (f"{err}", ERRCOL)
return 0
date = datetime.now().isoformat() + "Z"
@@ -466,13 +474,13 @@ def update_gpx_track(self):
self._last_track_update = datetime.now()
- def set_database_path(self, initdir=HOME) -> Path:
+ def set_database_path(self, initdir: Path = HOME) -> str | NoneType:
"""
Set database directory.
- :param str initdir: initial directory (HOME)
- :return: file path
- :rtype: str
+ :param Path initdir: initial directory (HOME)
+ :return: file path or None
+ :rtype: str | NoneType
"""
self._databasepath = filedialog.askdirectory(
diff --git a/src/pygpsclient/globals.py b/src/pygpsclient/globals.py
index 36aef7ef..22e4b5c1 100644
--- a/src/pygpsclient/globals.py
+++ b/src/pygpsclient/globals.py
@@ -73,7 +73,7 @@ def create_circle(self: Canvas, x: int, y: int, r: int, **kwargs):
MQTT_PROTOCOL = 64
TTY_PROTOCOL = 128
-# Various global constants
+# Various global constants - please keep in ascending alphabetical order
HOME = Path.home()
APPNAME = __name__.split(".", 1)[0] # i.e. "pygpsclient"
ASCII = "ascii"
@@ -90,7 +90,6 @@ def create_circle(self: Canvas, x: int, y: int, r: int, **kwargs):
4800,
)
BSR = "backslashreplace"
-CFG = "cfg"
CLASS = "cls"
COLORTAGS = "colortags"
CONFIGFILE = path.join(HOME, f"{APPNAME}.json")
@@ -109,13 +108,14 @@ def create_circle(self: Canvas, x: int, y: int, r: int, **kwargs):
4: "socket",
}
CRLF = b"\x0d\x0a"
+CUSTOM = "custom"
DDD = "DD.D"
DEFAULT_BUFSIZE = 4096
DEFAULT_PASSWORD = "password" # nosec
DEFAULT_PORT = 50010
-DEFAULT_TLS_PORTS = (443, 2102, 8443, 50443, 58443)
DEFAULT_REGION = "eu"
DEFAULT_SERVER = "localhost"
+DEFAULT_TLS_PORTS = (443, 2102, 8443, 50443, 58443)
DEFAULT_USER = "anon"
DIRNAME = path.dirname(__file__)
DISCONNECTED = 0
@@ -172,11 +172,10 @@ def create_circle(self: Canvas, x: int, y: int, r: int, **kwargs):
ICON_GITHUB = path.join(DIRNAME, "resources/github-256.png")
ICON_LOAD = path.join(DIRNAME, "resources/iconmonstr-folder-18-24.png")
ICON_LOGREAD = path.join(DIRNAME, "resources/binary-1-24.png")
-ICON_NMEACONFIG = path.join(DIRNAME, "resources/iconmonstr-gear-2-24-brown.png")
+ICON_NMEACONFIG = path.join(DIRNAME, "resources/iconmonstr-gear-2-24-nmea.png")
ICON_NOCLIENT = path.join(DIRNAME, "resources/iconmonstr-noclient-10-24.png")
ICON_NOTRANSMIT = path.join(DIRNAME, "resources/iconmonstr-notransmit-10-24.png")
ICON_NTRIPCONFIG = path.join(DIRNAME, "resources/iconmonstr-antenna-4-24.png")
-ICON_TTYCONFIG = path.join(DIRNAME, "resources/icon-tty-24-green.png")
ICON_PENDING = path.join(DIRNAME, "resources/iconmonstr-time-6-24.png")
ICON_PLAY = path.join(DIRNAME, "resources/iconmonstr-media-control-48-24.png")
ICON_POS = path.join(DIRNAME, "resources/iconmonstr-plus-lined-24.png")
@@ -188,42 +187,35 @@ def create_circle(self: Canvas, x: int, y: int, r: int, **kwargs):
ICON_SERIAL = path.join(DIRNAME, "resources/usbport-1-24.png")
ICON_SOCKET = path.join(DIRNAME, "resources/ethernet-1-24.png")
ICON_SPARTNCONFIG = path.join(DIRNAME, "resources/iconmonstr-antenna-3-24.png")
-# ICON_SPARTNDISABLE = path.join(DIRNAME, "resources/iconmonstr-antenna-3-24-greyed.png")
+ICON_SPONSOR = path.join(DIRNAME, "resources/bmac-logo-60.png")
ICON_START = path.join(DIRNAME, "resources/marker_start.png")
ICON_STOP = path.join(DIRNAME, "resources/iconmonstr-stop-1-24.png")
ICON_TRANSMIT = path.join(DIRNAME, "resources/iconmonstr-transmit-10-24.png")
-ICON_UBXCONFIG = path.join(DIRNAME, "resources/iconmonstr-gear-2-24.png")
+ICON_TTYCONFIG = path.join(DIRNAME, "resources/iconmonstr-gear-2-24-tty.png")
+ICON_UBXCONFIG = path.join(DIRNAME, "resources/iconmonstr-gear-2-24-ubx.png")
ICON_UNDO = path.join(DIRNAME, "resources/iconmonstr-undo-24.png")
ICON_UNKNOWN = path.join(DIRNAME, "resources/clear-1-24.png")
ICON_WARNING = path.join(DIRNAME, "resources/iconmonstr-warning-1-24.png")
IMG_WORLD = path.join(DIRNAME, "resources/world.png")
-ICON_SPONSOR = path.join(DIRNAME, "resources/bmac-logo-60.png")
IMG_WORLD_BOUNDS = Area(-90, -180, 90, 180)
-LBAND = "LBAND"
-LICENSE_URL = "https://github.com/semuconsulting/PyGPSClient/blob/master/LICENSE"
-MAPAPI_URL = "https://developer.mapquest.com/user/login/sign-up"
-MQTTIPMODE = 0
-MQTTLBANDMODE = 1
-MINHEIGHT = 600
-MINWIDTH = 800
-CUSTOM = "custom"
IMPORT = "import"
-WORLD = "world"
+LBAND = "LBAND"
LC29H = "Quectel LC29H"
LG290P = "Quectel LG29P/LG580P"
-MAX_SNR = 60 # upper limit of graphview snr axis
-MAXLOGLINES = 10000 # maximum number of 'lines' per datalog file
-MIN_GUI_UPDATE_INTERVAL = 0.1 # minimum GUI widget update interval (seconds)
+LICENSE_URL = "https://github.com/semuconsulting/PyGPSClient/blob/master/LICENSE"
+MAPAPI_URL = "https://developer.mapquest.com/user/login/sign-up"
+MAX_SNR = 60 # upper limit of levelsview CNo axis
MAXFLOAT = 2e20
+MAXLOGSIZE = 10485760 # maximum size of individual log file in bytes
+MIN_GUI_UPDATE_INTERVAL = 0.1 # minimum GUI widget update interval (seconds)
MINFLOAT = -MAXFLOAT
-MOSAIC_X5 = "Septentrio Mosaic X5"
+MINHEIGHT = 600
+MINWIDTH = 800
+MOSAIC_X5 = "Septentrio Mosaic X3/X5"
MQAPIKEY = "mqapikey"
-MSGMODES = {
- "GET": GET,
- "SET": SET,
- "POLL": POLL,
- "SETPOLL": SETPOLL,
-}
+MQTTIPMODE = 0
+MQTTLBANDMODE = 1
+MSGMODES = {"GET": GET, "SET": SET, "POLL": POLL, "SETPOLL": SETPOLL}
NOPORTS = 3
NTRIP = "NTRIP"
NTRIP_EVENT = "<>"
@@ -233,13 +225,12 @@ def create_circle(self: Canvas, x: int, y: int, r: int, **kwargs):
PYPI_URL = "https://pypi.org/pypi/PyGPSClient"
QUITONERRORDEFAULT = 1
RCVR_CONNECTION = "USB,UART1" # default GNSS receiver connection port(s)
-ROMVER_NEW = "23.01" # min device ROM version using configuration database
READONLY = "readonly"
RESIZE = "resize"
+ROMVER_NEW = "23.01" # min device ROM version using configuration database
ROUTE = "route"
RXMMSG = "RXM-SPARTN-KEY"
SAT_EXPIRY = 10 # how long passed satellites are kept in the sky and graph view
-
SCREENSCALE = 0.8 # screen resolution scaling factor
SOCK_NTRIP = "NTRIP CASTER"
SOCK_SERVER = "SOCKET SERVER"
@@ -250,13 +241,13 @@ def create_circle(self: Canvas, x: int, y: int, r: int, **kwargs):
SOCKSERVER_MAX_CLIENTS = 5
SOCKSERVER_NTRIP_PORT = 2101
SOCKSERVER_PORT = 50012
+SPARTN_BASEDATE_CURRENT = -1
+SPARTN_BASEDATE_DATASTREAM = 0
+SPARTN_DEFAULT_KEY = "abcd1234abcd1234abcd1234abcd1234"
SPARTN_EOF_EVENT = "<>"
SPARTN_ERR_EVENT = "<>"
SPARTN_EVENT = "<>"
SPARTN_KEYLEN = 16
-SPARTN_DEFAULT_KEY = "abcd1234abcd1234abcd1234abcd1234"
-SPARTN_BASEDATE_CURRENT = -1
-SPARTN_BASEDATE_DATASTREAM = 0
SPARTN_OUTPORT = 8883
SPARTN_PPREGIONS = ("eu", "us", "jp", "kr", "au")
SPARTN_PPSERVER_URL = "pp.services.u-blox.com"
@@ -265,19 +256,10 @@ def create_circle(self: Canvas, x: int, y: int, r: int, **kwargs):
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}
-THD = "thd"
TIME0 = datetime(1970, 1, 1) # basedate for time()
-TIMEOUTS = (
- "0.1",
- "0.2",
- "1",
- "2",
- "5",
- "10",
- "20",
- "None",
- "0",
-)
+TIMEOUTS = ("0.1", "0.2", "1", "2", "5", "10", "20", "None", "0")
+# map nmea talker to gnss_id
+TKGN = {"GN": 0, "GP": 0, "GA": 2, "GB": 3, "BD": 3, "GQ": 5, "GL": 6, "GI": 7}
TOPIC_IP = "/pp/ip/{}"
TOPIC_MGA = "/pp/ubx/mga"
TOPIC_RXM = "/pp/ubx/0236/ip"
@@ -305,6 +287,7 @@ def create_circle(self: Canvas, x: int, y: int, r: int, **kwargs):
WIDGETU3 = (800, 200) # Console size
WIDGETU4 = (500, 500) # GPX Track viewer size
WIDGETU6 = (400, 200) # Chart size
+WORLD = "world"
XML_HDR = ''
ZED_F9 = "u-blox ZED-F9"
ZED_X20 = "u-blox ZED-X20"
@@ -352,7 +335,6 @@ def create_circle(self: Canvas, x: int, y: int, r: int, **kwargs):
SERVERCONFIG = 18
SBF_MONHW = 19
-# keep KNOWNGPS all lower case
KNOWNGPS = (
"cp210",
"ft230",
@@ -375,13 +357,12 @@ def create_circle(self: Canvas, x: int, y: int, r: int, **kwargs):
"usb_to_uart",
"user-defined",
)
+"""
+Recognised GNSS device serial port designators.
+Used to 'auto-select' GNSS device in serial port list.
+(keep in lower-case)
+"""
-# map of fix values to descriptions
-# the keys in this map are a concatenation of NMEA/UBX
-# message identifier and attribute value e.g.:
-# GGA1: GGA + quality = 1
-# NAV-STATUS3: NAV-STATUS + gpsFix = 3
-# (valid for NMEA >=4)
FIXLOOKUP = {
"GGA1": "3D", # quality
"GGA2": "3D",
@@ -454,3 +435,11 @@ def create_circle(self: Canvas, x: int, y: int, r: int, **kwargs):
"PVTGeodetic8": "RTK FLOAT",
"PVTGeodetic10": "PPP",
}
+"""
+Map of fix values to descriptions.
+The keys in this map are a concatenation of NMEA/UBX
+message identifier and attribute value e.g.
+GGA1: GGA + quality = 1
+NAV-STATUS3: NAV-STATUS + gpsFix = 3
+(valid for NMEA >=4)
+"""
diff --git a/src/pygpsclient/gnss_status.py b/src/pygpsclient/gnss_status.py
index 761bfbb0..fb429463 100644
--- a/src/pygpsclient/gnss_status.py
+++ b/src/pygpsclient/gnss_status.py
@@ -54,14 +54,16 @@ def __init__(self):
self.acc_heading = 0.0 # rover relative position heading accuracy
self.acc_length = 0.0 # rover relative position distance accuracy
self.rel_pos_flags = [] # rover relative position flags
- self.gsv_data = {} # list of satellite tuples (gnssId, svid, elev, azim, cno)
+ # dict of satellite {(gnssid,svid}: (gnssId, svid, elev, azim, cno, last_updated)}
+ self.gsv_data = {}
+ # dict of hardware, firmware and software versions
self.version_data = {
"swversion": NA,
"hwversion": NA,
"fwversion": NA,
"romversion": NA,
"gnss": NA,
- } # dict of hardware, firmware and software versions
+ }
self.sysmon_data = {} # dict of system monitor data (cpu and memory load, etc.)
self.spectrum_data = [] # list of spectrum data (spec, spn, res, ctr, pga)
self.comms_data = {} # dict of comms port utilisation (tx and rx loads)
diff --git a/src/pygpsclient/gpx_dialog.py b/src/pygpsclient/gpx_dialog.py
index 24129d48..80189491 100644
--- a/src/pygpsclient/gpx_dialog.py
+++ b/src/pygpsclient/gpx_dialog.py
@@ -17,13 +17,13 @@
from statistics import mean, median
from tkinter import (
ALL,
+ EW,
+ NSEW,
Button,
- E,
Frame,
IntVar,
Label,
N,
- S,
Spinbox,
StringVar,
W,
@@ -214,15 +214,15 @@ def _do_layout(self):
Arrange widgets.
"""
- self._frm_body.grid(column=0, row=0, sticky=(N, S, E, W))
- self._frm_map.grid(column=0, row=0, sticky=(N, S, E, W))
- self._frm_profile.grid(column=0, row=1, sticky=(W, E))
- self._frm_info.grid(column=0, row=2, sticky=(W, E))
- self._frm_controls.grid(column=0, row=3, columnspan=7, sticky=(W, E))
- self._can_mapview.grid(column=0, row=0, sticky=(N, S, E, W))
- self._can_profile.grid(column=0, row=0, sticky=(N, S, E, W))
+ self._frm_body.grid(column=0, row=0, sticky=NSEW)
+ self._frm_map.grid(column=0, row=0, sticky=NSEW)
+ self._frm_profile.grid(column=0, row=1, sticky=EW)
+ self._frm_info.grid(column=0, row=2, sticky=EW)
+ self._frm_controls.grid(column=0, row=3, columnspan=7, sticky=EW)
+ self._can_mapview.grid(column=0, row=0, sticky=NSEW)
+ self._can_profile.grid(column=0, row=0, sticky=NSEW)
for i in range(MD_LINES):
- self._lbl_info[i].grid(column=0, row=i, padx=1, pady=1, sticky=(W, E))
+ self._lbl_info[i].grid(column=0, row=i, padx=1, pady=1, sticky=EW)
self._btn_load.grid(column=0, row=1, padx=3, pady=3)
self._lbl_maptype.grid(
column=1,
@@ -339,7 +339,7 @@ def _on_redraw(self, *args, **kwargs):
Handle redraw button press.
"""
- self.set_status(DLGGPXWAIT, INFOCOL)
+ self.status_label = (DLGGPXWAIT, INFOCOL)
self._detach_events()
self._reset()
self._spn_zoom.config(highlightbackground="gray90", highlightthickness=3)
@@ -355,6 +355,7 @@ def _open_gpxfile(self) -> str:
"""
return self.__app.file_handler.open_file(
+ self,
"gpx",
(("gpx files", "*.gpx"), ("all files", "*.*")),
)
@@ -367,7 +368,7 @@ def _on_load(self):
self._gpxfile = self._open_gpxfile()
if self._gpxfile is None: # user cancelled
return
- self.set_status(DLGGPXLOAD, INFOCOL)
+ self.status_label = (DLGGPXLOAD, INFOCOL)
self._parse_gpx()
def _parse_gpx(self):
@@ -381,7 +382,7 @@ def _parse_gpx(self):
trkpts = parser.getElementsByTagName(f"{ptyp}")
self._process_track(trkpts, ptyp)
except (TypeError, AttributeError, expat.ExpatError) as err:
- self.set_status(f"{DLGGPXERROR}\n{repr(err)}", ERRCOL)
+ self.status_label = (f"{DLGGPXERROR}\n{repr(err)}", ERRCOL)
self.logger.error(traceback.format_exc())
def _process_track(self, trkpts: list, ptyp: str):
@@ -396,7 +397,7 @@ def _process_track(self, trkpts: list, ptyp: str):
self._no_time = False
self._no_ele = False
if self._rng == 0:
- self.set_status(DLGGPXNULL.format(ptyp), ERRCOL)
+ self.status_label = (DLGGPXNULL.format(ptyp), ERRCOL)
return
minlat = minlon = 400
@@ -458,7 +459,7 @@ def _process_track(self, trkpts: list, ptyp: str):
self._draw_map()
self._draw_profile()
self._draw_metadata()
- self.set_status(DLGGPXLOADED, INFOCOL)
+ self.status_label = (DLGGPXLOADED, INFOCOL)
def _draw_map(self):
"""
diff --git a/src/pygpsclient/hardware_info_frame.py b/src/pygpsclient/hardware_info_frame.py
index 0a31aa3f..d9650a51 100644
--- a/src/pygpsclient/hardware_info_frame.py
+++ b/src/pygpsclient/hardware_info_frame.py
@@ -24,7 +24,6 @@
ICON_SEND,
ICON_WARNING,
NMEA_MONHW,
- OKCOL,
SBF_MONHW,
UBX_MONVER,
)
@@ -136,19 +135,17 @@ def _do_poll_ver(self, *args, **kwargs): # pylint: disable=unused-argument
if isinstance(msg, (NMEAMessage, UBXMessage)):
self.__app.send_to_device(msg.serialize())
- self.__app.set_status(
- f"{msg.identity} POLL message sent",
- )
+ self.__container.status_label = f"{msg.identity} POLL message sent"
elif isinstance(msg, bytes):
self.__app.send_to_device(msg)
- self.__app.set_status("Setup POLL message sent")
+ self.__container.status_label = "Setup POLL message sent"
self.__container.set_pending(pendmsg, penddlg)
- def update_status(self, msg: object):
+ def update_status(self, msg: UBXMessage | NMEAMessage):
"""
Update pending confirmation status.
- :param object msg: UBX or NMEA config message
+ :param UBXMessage | NMEAMessage msg: UBX or NMEA config message
"""
self._lbl_swver.config(
@@ -165,4 +162,4 @@ def update_status(self, msg: object):
)
self._lbl_gnss.config(text=self.__app.gnss_status.version_data.get("gnss", NA))
- self.__container.set_status(f"{msg.identity} GET message received", OKCOL)
+ self.__container.status_label = f"{msg.identity} GET message received"
diff --git a/src/pygpsclient/helpers.py b/src/pygpsclient/helpers.py
index 4cfc066c..70f0733f 100644
--- a/src/pygpsclient/helpers.py
+++ b/src/pygpsclient/helpers.py
@@ -315,30 +315,7 @@ def col2contrast(col: str) -> str:
r, g, b = str2rgb(col)
luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255
- col = "black" if luminance > 0.5 else "white"
- return col
-
-
-def config_nmea(state: int, port_type: str = "USB") -> UBXMessage:
- """
- Enable or disable NMEA messages at port level and use minimum UBX
- instead (NAV-PRT, NAV_SAT, NAV_DOP).
-
- :param int state: 1 = disable NMEA, 0 = enable NMEA
- :param str port_type: port that rcvr is connected on
- """
-
- nmea_state = 0 if state else 1
- layers = 1
- transaction = 0
- cfg_data = []
- cfg_data.append((f"CFG_{port_type}OUTPROT_NMEA", nmea_state))
- cfg_data.append((f"CFG_{port_type}OUTPROT_UBX", 1))
- cfg_data.append((f"CFG_MSGOUT_UBX_NAV_PVT_{port_type}", state))
- cfg_data.append((f"CFG_MSGOUT_UBX_NAV_DOP_{port_type}", state))
- cfg_data.append((f"CFG_MSGOUT_UBX_NAV_SAT_{port_type}", state * 4))
-
- return UBXMessage.config_set(layers, transaction, cfg_data)
+ return "black" if luminance > 0.5 else "white"
def corrage2int(code: int) -> int:
@@ -1261,6 +1238,18 @@ def time2str(tim: float, sformat: str = "%H:%M:%S") -> str:
return dt.strftime(sformat)
+def unused_sats(data: dict) -> int:
+ """
+ Get number of 'unused' sats in gnss_data.gsv_data.
+
+ :param dict data: {(gnssid,svid}: (gnssId, svid, elev, azim, cno, last_updated)}
+ :return: number of sats where cno = 0
+ :rtype: int
+ """
+
+ return sum(1 for (_, _, _, _, cno, _) in data.values() if cno == 0)
+
+
def ubx2preset(msgs: tuple, desc: str = "") -> str:
"""
Convert one or more UBXMessages to format suitable for adding to user-defined
diff --git a/src/pygpsclient/importmap_dialog.py b/src/pygpsclient/importmap_dialog.py
index 717e7b54..ebfc7c5c 100644
--- a/src/pygpsclient/importmap_dialog.py
+++ b/src/pygpsclient/importmap_dialog.py
@@ -15,7 +15,9 @@
from tkinter import (
ALL,
DISABLED,
+ EW,
NORMAL,
+ NSEW,
Button,
Checkbutton,
E,
@@ -23,8 +25,6 @@
Frame,
IntVar,
Label,
- N,
- S,
StringVar,
W,
)
@@ -42,7 +42,7 @@
BGCOL,
ERRCOL,
IMPORT,
- INFOCOL,
+ OKCOL,
TRACEMODE_WRITE,
VALFLOAT,
Area,
@@ -51,14 +51,6 @@
from pygpsclient.strings import DLGTIMPORTMAP
from pygpsclient.toplevel_dialog import ToplevelDialog
-# profile chart parameters:
-AXIS_XL = 35 # x axis left offset
-AXIS_XR = 35 # x axis right offset
-AXIS_Y = 15 # y axis bottom offset
-ELEAX_COL = "green4" # color of elevation plot axis
-ELE_COL = "palegreen3" # color of elevation plot
-SPD_COL = INFOCOL # color of speed plot
-MD_LINES = 2 # number of lines of metadata
MINDIM = (456, 418)
@@ -140,9 +132,9 @@ def _do_layout(self):
"""
Arrange widgets.
"""
- self._frm_body.grid(column=0, row=0, sticky=(N, S, E, W))
- self._can_mapview.grid(column=0, row=0, sticky=(N, S, E, W))
- self._frm_controls.grid(column=0, row=1, sticky=(W, E))
+ self._frm_body.grid(column=0, row=0, sticky=NSEW)
+ self._can_mapview.grid(column=0, row=0, sticky=NSEW)
+ self._frm_controls.grid(column=0, row=1, sticky=EW)
self._btn_load.grid(column=0, row=0, padx=3, pady=3)
self._btn_redraw.grid(column=1, row=0, padx=3, pady=3)
self._btn_import.grid(column=2, row=0, padx=3, pady=3)
@@ -189,12 +181,12 @@ def _reset(self):
self._can_mapview.delete(ALL)
# self._btn_import.config(state=DISABLED)
if not HASRASTERIO:
- self.set_status(
- "Warning: rasterio library is not installed - bounds must be entered manually",
- INFOCOL,
+ self.status_label = (
+ "Warning: rasterio library is not installed - "
+ "bounds must be entered manually"
)
else:
- self.set_status("")
+ self.status_label = ""
def _on_update(self, var, index, mode):
"""
@@ -217,10 +209,10 @@ def _valid_entries(self) -> bool:
valid = valid & self._ent_minlon.validate(VALFLOAT, -180, 180)
valid = valid & self._ent_maxlon.validate(-180, 180)
if valid:
- self.set_status("", INFOCOL)
+ self.status_label = ""
self._btn_import.config(state=NORMAL)
else:
- self.set_status("Error: invalid entry", ERRCOL)
+ self.status_label = ("Error: invalid entry", ERRCOL)
self._btn_import.config(state=DISABLED)
return valid
@@ -229,7 +221,7 @@ def _on_load(self):
Load custom map from file.
"""
- self.set_status("")
+ self.status_label = ""
self._custommap = self._open_mapfile()
if self._custommap is not None:
bounds = self._get_bounds(self._custommap)
@@ -257,9 +249,13 @@ def _on_redraw(self, *args, **kwargs):
def _open_mapfile(self) -> str:
"""
Open custom map file.
+
+ :return: fully qualified path to map file
+ :rtype: str
"""
return self.__app.file_handler.open_file(
+ self,
"tif",
(("GeoTiff files", "*.tif"), ("all files", "*.*")),
)
@@ -281,15 +277,15 @@ def _get_bounds(self, mappath) -> Area:
ras.crs.to_epsg(), 4326, *ras.bounds
)
except Exception: # pylint: disable=broad-exception-caught
- self.set_status(
+ self.status_label = (
"Warning: image is not georeferenced - bounds must be entered manually",
ERRCOL,
)
- self._lonmin.set(round(lonmin, 8))
- self._latmin.set(round(latmin, 8))
- self._lonmax.set(round(lonmax, 8))
- self._latmax.set(round(latmax, 8))
+ self._lonmin.set(str(round(lonmin, 8)))
+ self._latmin.set(str(round(latmin, 8)))
+ self._lonmax.set(str(round(lonmax, 8)))
+ self._latmax.set(str(round(latmax, 8)))
return Area(
float(self._latmin.get()),
float(self._lonmin.get()),
@@ -311,10 +307,10 @@ def _on_import(self):
latmax = float(self._latmax.get())
if lonmax + 180 <= lonmin + 180 or latmax + 90 <= latmin + 90:
- self.set_status("Error: minimum must be less than maximum", ERRCOL)
+ self.status_label = ("Error: minimum must be less than maximum", ERRCOL)
else:
usermaps = self.__app.configuration.get("usermaps_l")
idx = 0 if self._first.get() else len(usermaps) + 1
usermaps.insert(idx, [self._custommap, [latmin, lonmin, latmax, lonmax]])
self.__app.configuration.set("usermaps_l", usermaps)
- self.set_status("Custom map imported", INFOCOL)
+ self.status_label = ("Custom map imported", OKCOL)
diff --git a/src/pygpsclient/imu_frame.py b/src/pygpsclient/imu_frame.py
index 7f45699c..21b8cdb8 100644
--- a/src/pygpsclient/imu_frame.py
+++ b/src/pygpsclient/imu_frame.py
@@ -14,14 +14,13 @@
"""
from tkinter import (
+ EW,
+ NSEW,
NW,
Canvas,
- E,
Frame,
IntVar,
Label,
- N,
- S,
Spinbox,
StringVar,
TclError,
@@ -134,12 +133,12 @@ def _body(self):
textvariable=self._option,
state=READONLY,
)
- self.canvas.grid(column=0, row=0, columnspan=4, sticky=(N, S, E, W))
+ self.canvas.grid(column=0, row=0, columnspan=4, sticky=NSEW)
- self._lbl_range.grid(column=0, row=1, sticky=(W, E))
- self._spn_range.grid(column=1, row=1, sticky=(W, E))
- self._lbl_option.grid(column=2, row=1, sticky=(W, E))
- self._spn_option.grid(column=3, row=1, sticky=(W, E))
+ self._lbl_range.grid(column=0, row=1, sticky=EW)
+ self._spn_range.grid(column=1, row=1, sticky=EW)
+ self._lbl_option.grid(column=2, row=1, sticky=EW)
+ self._spn_option.grid(column=3, row=1, sticky=EW)
def reset(self):
"""
diff --git a/src/pygpsclient/levelsview_frame.py b/src/pygpsclient/levelsview_frame.py
index e591b2e5..911e5f70 100644
--- a/src/pygpsclient/levelsview_frame.py
+++ b/src/pygpsclient/levelsview_frame.py
@@ -14,7 +14,7 @@
# pylint: disable=no-member
-from tkinter import BOTH, NE, YES, Frame, font
+from tkinter import NE, NSEW, Frame, font
from pygpsclient.canvas_plot import (
TAG_DATA,
@@ -31,11 +31,11 @@
MAX_SNR,
WIDGETU2,
)
-from pygpsclient.helpers import col2contrast
+from pygpsclient.helpers import col2contrast, unused_sats
OL_WID = 1
FONTSCALELG = 40
-FONTSCALESV = 40
+FONTSCALESV = 30
class LevelsviewFrame(Frame):
@@ -74,7 +74,7 @@ def _body(self):
self._canvas = CanvasGraph(
self.__app, self, width=self.width, height=self.height, bg=BGCOL
)
- self._canvas.pack(fill=BOTH, expand=YES)
+ self._canvas.grid(column=0, row=0, sticky=NSEW)
def _attach_events(self):
"""
@@ -106,9 +106,9 @@ def init_frame(self):
self._canvas.create_graph(
xdatamax=10,
ydatamax=(MAX_SNR,),
- xtickmaj=2,
+ xtickmaj=5,
ytickmaj=int(MAX_SNR / 10),
- ylegend=("cno",),
+ ylegend=("C/N0 dBHz",),
ycol=(FGCOL,),
ylabels=True,
xangle=35,
@@ -157,10 +157,11 @@ def update_frame(self):
"""
data = self.__app.gnss_status.gsv_data
- siv = len(self.__app.gnss_status.gsv_data)
-
+ show_unused = self.__app.configuration.get("unusedsat_b")
+ siv = len(data)
if siv == 0:
return
+ siv = siv if show_unused else siv - unused_sats(data)
w, h = self.width, self.height
self.init_frame()
@@ -168,14 +169,16 @@ def update_frame(self):
offset = self._canvas.xoffl # AXIS_XL + 2
colwidth = (w - self._canvas.xoffl - self._canvas.xoffr + 1) / siv
# scale x axis label
- svfont = font.Font(size=int(min(w, h) / FONTSCALESV))
- for d in sorted(data.values()): # sort by ascending gnssid, svid
- gnssId, prn, _, _, snr = d
- if snr in ("", "0", 0):
- snr = 1 # show 'place marker' in graph
- else:
- snr = int(snr)
- snr_y = int(snr) * (h - self._canvas.yoffb - 1) / MAX_SNR
+ fsiz = min(w * 15 / siv, w, h)
+ svfont = font.Font(size=int(fsiz / FONTSCALESV))
+ for val in sorted(data.values()): # sort by ascending gnssid, svid
+ gnssId, prn, _, _, cno, _ = val
+ if cno == 0:
+ if show_unused:
+ cno = 1 # show 'place marker' in graph
+ else:
+ continue
+ snr_y = int(cno) * (h - self._canvas.yoffb - 1) / MAX_SNR
(_, ol_col) = GNSS_LIST[gnssId]
prn = f"{int(prn):02}"
self._canvas.create_rectangle(
@@ -190,7 +193,7 @@ def update_frame(self):
)
self._canvas.create_text(
offset + colwidth / 2,
- h - self._canvas.yoffb,
+ h - self._canvas.yoffb - 1,
text=prn,
fill=FGCOL,
font=svfont,
@@ -200,7 +203,7 @@ def update_frame(self):
)
offset += colwidth
- # self._canvas.update_idletasks()
+ self.update_idletasks()
def _on_resize(self, event): # pylint: disable=unused-argument
"""
diff --git a/src/pygpsclient/map_frame.py b/src/pygpsclient/map_frame.py
index 6eadd585..ce871a60 100644
--- a/src/pygpsclient/map_frame.py
+++ b/src/pygpsclient/map_frame.py
@@ -24,14 +24,13 @@
from time import time
from tkinter import (
DISABLED,
+ EW,
NORMAL,
+ NSEW,
Checkbutton,
- E,
Frame,
IntVar,
Label,
- N,
- S,
Spinbox,
StringVar,
W,
@@ -48,7 +47,7 @@
WORLD,
Point,
)
-from pygpsclient.mapquest import (
+from pygpsclient.mapquest_handler import (
MAX_ZOOM,
MIN_UPDATE_INTERVAL,
MIN_ZOOM,
@@ -156,8 +155,8 @@ def _do_layout(self):
Arrange widgets in frame.
"""
- self._can_mapview.grid(column=0, row=0, sticky=(N, S, E, W))
- self._frm_options.grid(column=0, row=1, sticky=(E, W))
+ self._can_mapview.grid(column=0, row=0, sticky=NSEW)
+ self._frm_options.grid(column=0, row=1, sticky=EW)
self._spn_maptype.grid(column=0, row=0, padx=1, sticky=W)
self._lbl_zoom.grid(column=1, row=0, padx=1, sticky=W)
self._spn_zoom.grid(column=2, row=0, padx=1, sticky=W)
diff --git a/src/pygpsclient/mapquest_handler.py b/src/pygpsclient/mapquest_handler.py
new file mode 100644
index 00000000..10397cf2
--- /dev/null
+++ b/src/pygpsclient/mapquest_handler.py
@@ -0,0 +1,239 @@
+"""
+mapquest_handler.py
+
+MapQuest API Constants and Methods.
+
+MapQuest polygon compression and decompression routines
+adapted from the original javascript examples:
+
+https://developer.mapquest.com/documentation/api/static-map/
+https://developer.mapquest.com/documentation/common/encode-decode/
+
+Created on 04 May 2023
+
+:author: semuadmin (Steve Smith)
+:copyright: 2020 semuadmin
+:license: BSD 3-Clause
+
+"""
+
+from pygpsclient.globals import Area
+
+# MapQuest API URLS:
+MAPQURL = (
+ "https://www.mapquestapi.com/staticmap/v5/map?"
+ + "key={key}&type={type}&size={width},{height}"
+)
+BBOX = "&boundingBox={lat2},{lon1},{lat1},{lon2}" # top left, bottom right
+HACC = "&shape=radius:{radius}|weight:1|fill:ccffff50|border:88888850|{lat},{lon}"
+LOC = "&locations={lat},{lon}|{marker}"
+LOCS = "&locations={lat1},{lon1}||{lat2},{lon2}&defaultMarker=marker-num"
+LOCICON = "marker-sm-616161-ff4444"
+MARGIN = "&margin={margin}" # bbox margin (default 50)
+RETINA = "@2x" # append to size clause for double the resolution
+SCALE = "&scalebar={scale}|bottom" # true or false
+TRK = "&shape=weight:2|border:{color}|{track}"
+ZOOM = "&zoom={zoom}" # zoom level 1-20
+
+POINTLIMIT = 500 # max number of shape points supported by MapQuest API
+MAPQTIMEOUT = 5
+# how frequently the mapquest api is called to update the web map (seconds)
+MAP_UPDATE_INTERVAL = 60
+MIN_UPDATE_INTERVAL = 5
+MAX_ZOOM = 20
+MIN_ZOOM = 1
+TRKCOL = "ff00ff"
+# MapQuest static API map types ("dark" and "light" not used here)
+MAP = "map"
+SAT = "sat"
+HYB = "hyb"
+
+
+def compress_track(track: tuple, precision: int = 6, limit: int = POINTLIMIT) -> str:
+ """
+ Convert track to compressed Mapquest format.
+
+ :param tuple track: tuple of Points
+ :param int precision: no decimal places precision (6)
+ :param int limit: max no of points (500)
+ :return: compressed track
+ :rtype: str
+ """
+
+ # if the number of trackpoints exceeds the MapQuest API limit,
+ # increase step count until the number is within limits
+ points = []
+ stp = 1
+ rng = len(track)
+ while rng / stp > limit:
+ stp += 1
+ for i, p in enumerate(track):
+ if i % stp == 0:
+ points.append(p.lat)
+ points.append(p.lon)
+
+ # compress polygon for MapQuest API
+ return mapq_compress(points, precision)
+
+
+def format_mapquest_request(
+ mqapikey: str,
+ maptype: str,
+ width: int,
+ height: int,
+ zoom: int,
+ locations: list,
+ bbox: Area = None,
+ hacc: float = 0,
+) -> str:
+ """
+ Formats URL for web map download.
+
+ :param str mqapikey: MapQuest API key
+ :param str maptype: "map" or "sat"
+ :param int width: width of canvas
+ :param int height: height of canvas
+ :param int zoom: zoom factor
+ :param list locations: list of Points
+ :param Area bbox: bounding box (will override zoom)
+ :param float hacc: horizontal accuracy
+ :return: formatted MapQuest URL
+ :rtype: str
+ """
+ # pylint: disable=too-many-arguments, too-many-positional-arguments
+
+ url = MAPQURL.format(key=mqapikey, type=maptype, width=width, height=height)
+ radius = str(hacc / 1000) # km
+ zoom = min(20, zoom)
+
+ if bbox is None: # use location and zoom level
+ url += ZOOM.format(zoom=zoom)
+ else: # use bounding box (remember upper left, bottom right)
+ url += BBOX.format(
+ lat2=bbox.lat2, lon1=bbox.lon1, lat1=bbox.lat1, lon2=bbox.lon2
+ ) + MARGIN.format(margin=0)
+
+ if isinstance(locations, list): # at least one location
+ if len(locations) > 1: # multiple locations (track)
+ comp = compress_track(locations)
+ url += LOCS.format(
+ lat1=locations[0].lat,
+ lon1=locations[0].lon,
+ lat2=locations[-1].lat,
+ lon2=locations[-1].lon,
+ ) + TRK.format(color=TRKCOL, track=f"cmp6|enc:{comp}")
+ else: # single location
+ url += LOC.format(
+ lat=locations[0].lat, lon=locations[0].lon, marker=LOCICON
+ )
+
+ if hacc > 0:
+ url += HACC.format(
+ radius=radius, lat=locations[0].lat, lon=locations[0].lon
+ )
+
+ # seems to be bug in MapQuest API which causes error
+ # if scalebar displayed at maximum zoom
+ url += SCALE.format(scale=("true" if zoom < 20 else "false"))
+
+ return url
+
+
+def mapq_encode(num: int) -> str:
+ """
+ Encode number representing character.
+
+ :param int num: number to encode
+ :return: encoded number as string
+ :rtype: str
+ """
+
+ num = num << 1
+ if num < 0:
+ num = ~(num)
+
+ encoded = ""
+ while num >= 0x20:
+ encoded += chr((0x20 | (num & 0x1F)) + 63)
+ num >>= 5
+
+ encoded += chr(num + 63)
+ return encoded
+
+
+def mapq_decompress(encoded: str, precision: int = 6) -> list:
+ """
+ Decompress polygon for MapQuest API.
+
+ :param str encoded: polygon encoded as string
+ :param int precision: no decimal places precision (6)
+ :return: polygon as list of point tuples (lat,lon)
+ :rtype: list
+ """
+
+ precision = 10**-precision
+ leng = len(encoded)
+ index = 0
+ lat = 0
+ lng = 0
+ array = []
+ while index < leng:
+ shift = 0
+ result = 0
+ b = 0xFF
+ while b >= 0x20:
+ b = ord(encoded[index]) - 63
+ index += 1
+ result |= (b & 0x1F) << shift
+ shift += 5
+
+ dlat = ~(result >> 1) if (result & 1) else (result >> 1)
+ lat += dlat
+ shift = 0
+ result = 0
+ b = 0xFF
+ while b >= 0x20:
+ b = ord(encoded[index]) - 63
+ index += 1
+ result |= (b & 0x1F) << shift
+ shift += 5
+
+ dlng = ~(result >> 1) if (result & 1) else (result >> 1)
+ lng += dlng
+ array.append(lat * precision)
+ array.append(lng * precision)
+
+ return array
+
+
+def mapq_compress(points: list, precision: int = 6) -> str:
+ """
+ Compress polygon for MapQuest API.
+
+ :param list points: polygon as list of point tuples (lat, lon)
+ :param int precision: no decimal places precision (6)
+ :return: polygon encoded as string
+ :rtype: string
+ """
+
+ oldLat = 0
+ oldLng = 0
+ leng = len(points)
+ index = 0
+ encoded = ""
+ precision = 10**precision
+ while index < leng:
+ # Round to N decimal places
+ lat = round(points[index] * precision)
+ index += 1
+ lng = round(points[index] * precision)
+ index += 1
+
+ # Encode the differences between the points
+ encoded += mapq_encode(lat - oldLat)
+ encoded += mapq_encode(lng - oldLng)
+
+ oldLat = lat
+ oldLng = lng
+
+ return encoded
diff --git a/src/pygpsclient/nmea_config_dialog.py b/src/pygpsclient/nmea_config_dialog.py
index c2723130..f8a92947 100644
--- a/src/pygpsclient/nmea_config_dialog.py
+++ b/src/pygpsclient/nmea_config_dialog.py
@@ -15,7 +15,7 @@
:license: BSD 3-Clause
"""
-from tkinter import E, N, S, W
+from tkinter import NSEW
from pynmeagps import NMEAMessage
@@ -99,7 +99,7 @@ def _do_layout(self):
row=row,
columnspan=colsp,
rowspan=rowsp,
- sticky=(N, S, W, E),
+ sticky=NSEW,
)
row += rowsp
# right column of grid
@@ -112,7 +112,7 @@ def _do_layout(self):
row=row,
columnspan=colsp,
rowspan=rowsp,
- sticky=(N, S, W, E),
+ sticky=NSEW,
)
row += rowsp
@@ -128,7 +128,7 @@ def _reset(self):
CONNECTED_SOCKET,
CONNECTED_SIMULATOR,
):
- self.set_status("Device not connected", ERRCOL)
+ self.status_label = ("Device not connected", ERRCOL)
def _attach_events(self):
"""
diff --git a/src/pygpsclient/nmea_handler.py b/src/pygpsclient/nmea_handler.py
index 2b6f9692..51db874e 100644
--- a/src/pygpsclient/nmea_handler.py
+++ b/src/pygpsclient/nmea_handler.py
@@ -23,7 +23,7 @@
from pynmeagps import NMEAMessage
from pyubx2 import itow2utc
-from pygpsclient.globals import SAT_EXPIRY
+from pygpsclient.globals import SAT_EXPIRY, TKGN
from pygpsclient.helpers import fix2desc, kmph2ms, knots2ms, svid2gnssid
from pygpsclient.strings import DLGTNMEA
@@ -219,46 +219,38 @@ def _process_GSV(self, data: NMEAMessage):
:param pynmeagps.NMEAMessage data: parsed GSV sentence
"""
- show_unused = self.__app.configuration.get("unusedsat_b")
- self.gsv_data = {}
- gsv_dict = {}
+ gnss = TKGN.get(data.talker, 0)
now = time()
- if data.talker == "GA":
- gnss = 2 # Galileo
- elif data.talker in ("GB", "BD"):
- gnss = 3 # Beidou (only available in MMEA 4.11)
- elif data.talker == "GL":
- gnss = 6 # GLONASS
- elif data.talker == "GI":
- gnss = 7 # NAVIC
- else:
- gnss = 0 # GPS, SBAS, QZSS
for i in range(4):
idx = f"_{i+1:02d}"
svid = getattr(data, "svid" + idx, "")
+ elev = getattr(data, "elv" + idx, 0)
+ azim = getattr(data, "az" + idx, 0)
+ cno = getattr(data, "cno" + idx, 0)
+ if not isinstance(cno, (int, float)):
+ cno = 0
if svid != "":
- key = f"{gnss}-{svid}"
- gsv_dict[key] = (
+ svid = int(svid)
+ if gnss == 0 and (120 <= svid <= 158):
+ gnss = 1 # SBAS
+ self.__app.gnss_status.gsv_data[(gnss, svid)] = (
gnss,
svid,
- getattr(data, "elv" + idx),
- getattr(data, "az" + idx),
- str(getattr(data, "cno" + idx)),
+ elev,
+ azim,
+ cno,
now,
)
- for key, value in gsv_dict.items():
- self.gsv_log[key] = value
+ # discard sats that haven't been seen in a while
+ for key, (_, _, _, _, _, lastupdate) in list(
+ self.__app.gnss_status.gsv_data.items()
+ ):
+ if now - lastupdate > SAT_EXPIRY:
+ del self.__app.gnss_status.gsv_data[key]
- for key, (gnssId, svid, elev, azim, cno, lastupdate) in self.gsv_log.items():
- if cno in ("", "0", 0) and not show_unused: # omit unused sats
- continue
- if now - lastupdate < SAT_EXPIRY: # expire passed sats
- self.gsv_data[key] = (gnssId, svid, elev, azim, cno)
-
- self.__app.gnss_status.siv = len(self.gsv_data)
- self.__app.gnss_status.gsv_data = self.gsv_data
+ self.__app.gnss_status.siv = len(self.__app.gnss_status.gsv_data)
def _process_VTG(self, data: NMEAMessage):
"""
diff --git a/src/pygpsclient/nmea_preset_frame.py b/src/pygpsclient/nmea_preset_frame.py
index c57eede9..f6efeabd 100644
--- a/src/pygpsclient/nmea_preset_frame.py
+++ b/src/pygpsclient/nmea_preset_frame.py
@@ -12,6 +12,7 @@
import logging
from tkinter import (
+ EW,
HORIZONTAL,
LEFT,
VERTICAL,
@@ -118,15 +119,15 @@ def _do_layout(self):
Layout widgets.
"""
- self._lbl_presets.grid(column=0, row=0, columnspan=6, padx=3, sticky=(W, E))
+ self._lbl_presets.grid(column=0, row=0, columnspan=6, padx=3, sticky=EW)
self._lbx_preset.grid(
- column=0, row=1, columnspan=3, rowspan=20, padx=3, pady=3, sticky=(W, E)
+ column=0, row=1, columnspan=3, rowspan=20, padx=3, pady=3, sticky=EW
)
self._scr_presetv.grid(column=2, row=1, rowspan=20, sticky=(N, S, E))
- self._scr_preseth.grid(column=0, row=21, columnspan=3, sticky=(W, E))
+ self._scr_preseth.grid(column=0, row=21, columnspan=3, sticky=EW)
self._btn_send_command.grid(column=3, row=1, padx=3, ipadx=3, ipady=3, sticky=E)
self._lbl_send_command.grid(
- column=3, row=2, padx=3, ipadx=3, ipady=3, sticky=(W, E)
+ column=3, row=2, padx=3, ipadx=3, ipady=3, sticky=EW
)
(cols, rows) = self.grid_size()
@@ -166,7 +167,7 @@ def _on_send_preset(self, *args, **kwargs): # pylint: disable=unused-argument
"""
if self._preset_command in ("", None):
- self.__container.set_status("Select preset", ERRCOL)
+ self.__container.status_label = ("Select preset", ERRCOL)
return
confids = []
@@ -183,22 +184,16 @@ def _on_send_preset(self, *args, **kwargs): # pylint: disable=unused-argument
if status == CONFIRMED:
self._lbl_send_command.config(image=self._img_pending)
- self.__container.set_status(
- "Command(s) sent",
- )
+ self.__container.status_label = "Command(s) sent"
for msgid in confids:
self.__container.set_pending(msgid, NMEA_PRESET)
elif status == CANCELLED:
- self.__container.set_status(
- "Command(s) cancelled",
- )
+ self.__container.status_label = "Command(s) cancelled"
elif status == NOMINAL:
- self.__container.set_status(
- "Command(s) sent, no results",
- )
+ self.__container.status_label = "Command(s) sent, no results"
except Exception as err: # pylint: disable=broad-except
- self.__container.set_status(f"Error {err}", ERRCOL)
+ self.__container.status_label = (f"Error {err}", ERRCOL)
self._lbl_send_command.config(image=self._img_warn)
def _do_user_defined(self, command: str) -> list:
@@ -231,7 +226,7 @@ def _do_user_defined(self, command: str) -> list:
# self.logger.debug(f"{str(msg)=} - {msg.serialize()=} {confids=}")
self.__container.send_command(msg)
except Exception as err: # pylint: disable=broad-except
- self.__app.set_status(f"Error {err}", ERRCOL)
+ self.__container.status_label = (f"Error {err}", ERRCOL)
self._lbl_send_command.config(image=self._img_warn)
return confids
@@ -246,7 +241,7 @@ def update_status(self, msg: NMEAMessage):
status = getattr(msg, "status", "OK")
if status == "OK":
self._lbl_send_command.config(image=self._img_confirmed)
- self.__container.set_status("Preset command(s) acknowledged", OKCOL)
+ self.__container.status_label = ("Preset command(s) acknowledged", OKCOL)
elif status == "ERROR":
self._lbl_send_command.config(image=self._img_warn)
- self.__container.set_status("Preset command(s) rejected", ERRCOL)
+ self.__container.status_label = ("Preset command(s) rejected", ERRCOL)
diff --git a/src/pygpsclient/ntrip_client_dialog.py b/src/pygpsclient/ntrip_client_dialog.py
index 0a2c6e10..19ae9a0d 100644
--- a/src/pygpsclient/ntrip_client_dialog.py
+++ b/src/pygpsclient/ntrip_client_dialog.py
@@ -21,19 +21,19 @@
from tkinter import (
DISABLED,
END,
+ EW,
HORIZONTAL,
NORMAL,
+ NS,
+ NSEW,
VERTICAL,
Button,
- E,
Entry,
Frame,
IntVar,
Label,
Listbox,
- N,
Radiobutton,
- S,
Scrollbar,
Spinbox,
StringVar,
@@ -293,24 +293,24 @@ def _do_layout(self):
"""
# top of grid
- self._frm_body.grid(column=0, row=0, sticky=(N, S, E, W))
+ self._frm_body.grid(column=0, row=0, sticky=NSEW)
# body of grid
self._frm_socket.grid(
column=0, row=0, columnspan=3, rowspan=3, padx=3, pady=3, sticky=W
)
ttk.Separator(self._frm_body).grid(
- column=0, row=3, columnspan=5, padx=3, pady=3, sticky=(W, E)
+ column=0, row=3, columnspan=5, padx=3, pady=3, sticky=EW
)
self._lbl_mountpoint.grid(column=0, row=4, padx=3, pady=3, sticky=W)
self._ent_mountpoint.grid(column=1, row=4, padx=3, pady=3, sticky=W)
self._lbl_mpdist.grid(column=2, row=4, columnspan=2, padx=3, pady=3, sticky=W)
self._lbl_sourcetable.grid(column=0, row=5, padx=3, pady=3, sticky=W)
self._lbx_sourcetable.grid(
- column=1, row=5, columnspan=3, rowspan=4, padx=3, pady=3, sticky=(E, W)
+ column=1, row=5, columnspan=3, rowspan=4, padx=3, pady=3, sticky=EW
)
- self._scr_sourcetablev.grid(column=4, row=5, rowspan=4, sticky=(N, S))
- self._scr_sourcetableh.grid(column=1, columnspan=3, row=9, sticky=(E, W))
+ self._scr_sourcetablev.grid(column=4, row=5, rowspan=4, sticky=NS)
+ self._scr_sourcetableh.grid(column=1, columnspan=3, row=9, sticky=EW)
self._lbl_ntripversion.grid(column=0, row=10, padx=3, pady=3, sticky=W)
self._spn_ntripversion.grid(column=1, row=10, padx=3, pady=3, sticky=W)
self._lbl_datatype.grid(column=0, row=11, padx=3, pady=3, sticky=W)
@@ -322,7 +322,7 @@ def _do_layout(self):
column=1, row=13, columnspan=2, padx=3, pady=3, sticky=W
)
ttk.Separator(self._frm_body).grid(
- column=0, row=14, columnspan=5, padx=3, pady=3, sticky=(W, E)
+ column=0, row=14, columnspan=5, padx=3, pady=3, sticky=EW
)
self._lbl_ntripggaint.grid(column=0, row=15, padx=2, pady=3, sticky=W)
self._spn_ntripggaint.grid(column=1, row=15, padx=3, pady=2, sticky=W)
@@ -337,7 +337,7 @@ def _do_layout(self):
self._lbl_sep.grid(column=2, row=18, padx=3, pady=2, sticky=W)
self._ent_sep.grid(column=3, row=18, columnspan=2, padx=3, pady=2, sticky=W)
ttk.Separator(self._frm_body).grid(
- column=0, row=19, columnspan=5, padx=3, pady=3, sticky=(W, E)
+ column=0, row=19, columnspan=5, padx=3, pady=3, sticky=EW
)
self._btn_connect.grid(column=0, row=20, padx=3, pady=3, sticky=W)
self._btn_disconnect.grid(column=1, row=20, padx=3, pady=3, sticky=W)
@@ -429,12 +429,12 @@ def set_controls(self, connected: bool, msgt: tuple = None):
else "Disconnected"
)
if msgt is None:
- self.set_status(msg, INFOCOL)
+ self.status_label = (msg, INFOCOL)
else:
msg, col = msgt
- self.set_status(msg, col)
+ self.status_label = (msg, col)
- self._frm_socket.set_status(connected)
+ self._frm_socket.status_label = connected
self._btn_disconnect.config(state=(NORMAL if connected else DISABLED))
@@ -474,20 +474,6 @@ def set_controls(self, connected: bool, msgt: tuple = None):
except TclError: # fudge during thread termination
pass
- def set_status(self, message: str, color: str = ""):
- """
- Set status message.
-
- :param str message: message to be displayed
- :param str color: rgb color of text
- """
-
- color = INFOCOL if color == "blue" else color
- message = f"{message[:78]}.." if len(message) > 80 else message
- if color != "":
- self._lbl_status.config(fg=color)
- self._status.set(" " + message)
-
def _on_select_mp(self, event):
"""
Mountpoint has been selected from listbox; set
@@ -700,7 +686,7 @@ def _valid_settings(self) -> bool:
valid = valid & self._ent_sep.validate(VALFLOAT, -MAXALT, MAXALT)
if not valid:
- self.set_status("ERROR - invalid settings", ERRCOL)
+ self.status_label = ("ERROR - invalid settings", ERRCOL)
return valid
diff --git a/src/pygpsclient/receiver_config_handler.py b/src/pygpsclient/receiver_config_handler.py
index c4cdc3a9..b7612a80 100644
--- a/src/pygpsclient/receiver_config_handler.py
+++ b/src/pygpsclient/receiver_config_handler.py
@@ -19,6 +19,28 @@
from pygpsclient.helpers import val2sphp
+def config_nmea(state: int, port_type: str = "USB") -> UBXMessage:
+ """
+ Enable or disable NMEA messages at port level and use minimum UBX
+ instead (NAV-PRT, NAV_SAT, NAV_DOP).
+
+ :param int state: 1 = disable NMEA, 0 = enable NMEA
+ :param str port_type: port that rcvr is connected on
+ """
+
+ nmea_state = 0 if state else 1
+ layers = 1
+ transaction = 0
+ cfg_data = []
+ cfg_data.append((f"CFG_{port_type}OUTPROT_NMEA", nmea_state))
+ cfg_data.append((f"CFG_{port_type}OUTPROT_UBX", 1))
+ cfg_data.append((f"CFG_MSGOUT_UBX_NAV_PVT_{port_type}", state))
+ cfg_data.append((f"CFG_MSGOUT_UBX_NAV_DOP_{port_type}", state))
+ cfg_data.append((f"CFG_MSGOUT_UBX_NAV_SAT_{port_type}", state * 4))
+
+ return UBXMessage.config_set(layers, transaction, cfg_data)
+
+
def config_disable_ublox() -> UBXMessage:
"""
Disable base station mode for u-blox receivers.
diff --git a/src/pygpsclient/resources/icon-tty-24-green.png b/src/pygpsclient/resources/icon-tty-24-green.png
deleted file mode 100644
index 2a96ee75..00000000
Binary files a/src/pygpsclient/resources/icon-tty-24-green.png and /dev/null differ
diff --git a/src/pygpsclient/resources/iconmonstr-gear-2-24-brown.png b/src/pygpsclient/resources/iconmonstr-gear-2-24-brown.png
deleted file mode 100644
index 8fa75b21..00000000
Binary files a/src/pygpsclient/resources/iconmonstr-gear-2-24-brown.png and /dev/null differ
diff --git a/src/pygpsclient/resources/iconmonstr-gear-2-24-nmea.png b/src/pygpsclient/resources/iconmonstr-gear-2-24-nmea.png
new file mode 100644
index 00000000..9e38440c
Binary files /dev/null and b/src/pygpsclient/resources/iconmonstr-gear-2-24-nmea.png differ
diff --git a/src/pygpsclient/resources/iconmonstr-gear-2-24-tty.png b/src/pygpsclient/resources/iconmonstr-gear-2-24-tty.png
new file mode 100644
index 00000000..40f6e05e
Binary files /dev/null and b/src/pygpsclient/resources/iconmonstr-gear-2-24-tty.png differ
diff --git a/src/pygpsclient/resources/iconmonstr-gear-2-24-ubx.png b/src/pygpsclient/resources/iconmonstr-gear-2-24-ubx.png
new file mode 100644
index 00000000..864a5c16
Binary files /dev/null and b/src/pygpsclient/resources/iconmonstr-gear-2-24-ubx.png differ
diff --git a/src/pygpsclient/resources/iconmonstr-gear-2-24.png b/src/pygpsclient/resources/iconmonstr-gear-2-24.png
deleted file mode 100644
index 67382b4a..00000000
Binary files a/src/pygpsclient/resources/iconmonstr-gear-2-24.png and /dev/null differ
diff --git a/src/pygpsclient/rover_frame.py b/src/pygpsclient/rover_frame.py
index a09674c1..51113cf7 100644
--- a/src/pygpsclient/rover_frame.py
+++ b/src/pygpsclient/rover_frame.py
@@ -15,7 +15,7 @@
# pylint: disable=invalid-name, no-member
-from tkinter import BOTH, NW, SW, YES, Frame
+from tkinter import NSEW, NW, SW, Frame
from pygpsclient.canvas_plot import (
MODE_POL,
@@ -79,7 +79,7 @@ def _body(self):
self._canvas = CanvasCompass(
self.__app, self, MODE_POL, width=self.width, height=self.height, bg=BGCOL
)
- self._canvas.pack(fill=BOTH, expand=YES)
+ self._canvas.grid(column=0, row=0, sticky=NSEW)
def _attach_events(self):
"""
@@ -194,6 +194,7 @@ def update_frame(self):
outline=TRKCOL,
tags=TAG_DATA,
)
+ self.update_idletasks()
# plot latest relative position with accuracy radius
x, y = self._canvas.d2xy(hdg, dis / self._scale_c)
diff --git a/src/pygpsclient/rtcm3_handler.py b/src/pygpsclient/rtcm3_handler.py
index df3cc06b..e12624e9 100644
--- a/src/pygpsclient/rtcm3_handler.py
+++ b/src/pygpsclient/rtcm3_handler.py
@@ -54,7 +54,6 @@ def process_data(self, raw_data: bytes, parsed_data: object):
self._process_1005(parsed_data)
except ValueError:
- # self.__app.set_status(RTCMVALERROR.format(err), ERRCOL)
pass
def _process_1005(self, parsed: RTCMMessage):
diff --git a/src/pygpsclient/scatter_frame.py b/src/pygpsclient/scatter_frame.py
index 839925f1..cd6b875e 100644
--- a/src/pygpsclient/scatter_frame.py
+++ b/src/pygpsclient/scatter_frame.py
@@ -23,20 +23,18 @@
# pylint: disable=no-member
from tkinter import (
+ EW,
HORIZONTAL,
+ NSEW,
NW,
Checkbutton,
- E,
Entry,
Frame,
IntVar,
- N,
- S,
Scale,
Spinbox,
StringVar,
TclError,
- W,
)
try:
@@ -80,9 +78,9 @@
INTS = (1, 2, 5, 10, 20, 50, 100)
FIXCOL = "#00EE00"
PNTTOPCOL = "#FF0000"
-CULLMID = True # whether to cull random points from middle of array
+CULLMID = False # whether to cull random points from middle of array
FIXINAUTO = False # whether to include fixed ref point in autorange
-MAXPOINTS = 500
+MAXPOINTS = 500 # maximum number of in-memory points before truncation
PNT = "pnt"
STDINT = 10 # standard deviation calculation interval
@@ -222,13 +220,13 @@ def _body(self):
variable=self._scale,
showvalue=False,
)
- self._canvas.grid(column=0, row=0, columnspan=3, sticky=(N, S, E, W))
- self._ent_reflat.grid(column=0, row=1, sticky=(W, E))
- self._ent_reflon.grid(column=1, row=1, sticky=(W, E))
- self._spn_center.grid(column=2, row=1, sticky=(W, E))
- self._chk_autorange.grid(column=0, row=2, sticky=(W, E))
- self._spn_interval.grid(column=1, row=2, sticky=(W, E))
- self._scl_range.grid(column=2, row=2, sticky=(W, E))
+ self._canvas.grid(column=0, row=0, columnspan=3, sticky=NSEW)
+ self._ent_reflat.grid(column=0, row=1, sticky=EW)
+ self._ent_reflon.grid(column=1, row=1, sticky=EW)
+ self._spn_center.grid(column=2, row=1, sticky=EW)
+ self._chk_autorange.grid(column=0, row=2, sticky=EW)
+ self._spn_interval.grid(column=1, row=2, sticky=EW)
+ self._scl_range.grid(column=2, row=2, sticky=EW)
def reset(self):
"""
@@ -489,6 +487,7 @@ def _update_plot(self):
if i == lp:
break
self._draw_point(pnt, PNTCOL)
+ self.update_idletasks()
if self._fixed is not None:
self._draw_point(self._fixed, FIXCOL, 3)
self._draw_point(self._points[-1], PNTTOPCOL)
diff --git a/src/pygpsclient/serialconfig_frame.py b/src/pygpsclient/serialconfig_frame.py
index fa79e599..d1e9a89a 100644
--- a/src/pygpsclient/serialconfig_frame.py
+++ b/src/pygpsclient/serialconfig_frame.py
@@ -20,9 +20,11 @@
from tkinter import (
DISABLED,
+ EW,
HORIZONTAL,
LEFT,
NORMAL,
+ NS,
VERTICAL,
Button,
Checkbutton,
@@ -33,8 +35,6 @@
IntVar,
Label,
Listbox,
- N,
- S,
Scrollbar,
Spinbox,
StringVar,
@@ -260,13 +260,11 @@ def _do_layout(self):
Layout widgets.
"""
- self._frm_basic.grid(column=0, row=0, columnspan=4, sticky=(W, E))
+ self._frm_basic.grid(column=0, row=0, columnspan=4, sticky=EW)
self._lbl_port.grid(column=0, row=0, sticky=W)
- self._lbx_port.grid(
- column=1, row=0, columnspan=3, sticky=(W, E), padx=3, pady=2
- )
- self._scr_portv.grid(column=4, row=0, sticky=(N, S))
- self._scr_porth.grid(column=1, row=1, columnspan=3, sticky=(E, W))
+ self._lbx_port.grid(column=1, row=0, columnspan=3, sticky=EW, padx=3, pady=2)
+ self._scr_portv.grid(column=4, row=0, sticky=NS)
+ self._scr_porth.grid(column=1, row=1, columnspan=3, sticky=EW)
self._lbl_bpsrate.grid(column=0, row=2, sticky=W)
self._spn_bpsrate.grid(column=1, row=2, sticky=W, padx=3, pady=2)
self._btn_refresh.grid(column=3, row=2, sticky=E)
@@ -506,7 +504,7 @@ def _on_toggle_advanced(self):
self._show_advanced = not self._show_advanced
if self._show_advanced:
- self._frm_advanced.grid(column=0, row=1, columnspan=3, sticky=(W, E))
+ self._frm_advanced.grid(column=0, row=1, columnspan=3, sticky=EW)
self._btn_toggle.config(image=self._img_contract)
else:
self._frm_advanced.grid_forget()
diff --git a/src/pygpsclient/serverconfig_frame.py b/src/pygpsclient/serverconfig_frame.py
index c6959a37..b7dec45b 100644
--- a/src/pygpsclient/serverconfig_frame.py
+++ b/src/pygpsclient/serverconfig_frame.py
@@ -17,12 +17,13 @@
:license: BSD 3-Clause
"""
-# pylint: disable=unused-argument
+# pylint: disable=unused-argument, too-many-lines
import logging
from time import sleep
from tkinter import (
DISABLED,
+ EW,
NORMAL,
Button,
Checkbutton,
@@ -69,7 +70,6 @@
)
from pygpsclient.helpers import (
MAXPORT,
- config_nmea,
lanip,
publicip,
)
@@ -82,6 +82,7 @@
config_fixed_lg290p,
config_fixed_septentrio,
config_fixed_ublox,
+ config_nmea,
config_svin_lc29h,
config_svin_lg290p,
config_svin_quectel,
@@ -159,8 +160,7 @@ def __init__(self, app, container, *args, **kwargs):
self.sock_port = StringVar()
self.sock_host = StringVar()
self.sock_mode = StringVar()
- self._sock_clients = StringVar()
- # self._set_basemode = IntVar()
+ self._sock_clients = IntVar()
self.receiver_type = StringVar()
self.base_mode = StringVar()
self.https = IntVar()
@@ -401,7 +401,7 @@ def _do_layout(self):
Layout widgets.
"""
- self._frm_basic.grid(column=0, row=0, columnspan=5, sticky=(W, E))
+ self._frm_basic.grid(column=0, row=0, columnspan=5, sticky=EW)
self._chk_socketserve.grid(
column=0, row=0, columnspan=2, rowspan=2, padx=2, pady=1, sticky=W
)
@@ -452,7 +452,7 @@ def reset(self):
pem, pemexists = check_pemfile()
if https and not pemexists:
err = DLGNOTLS.format(hostpem=pem)
- self.__app.set_status(err, ERRCOL)
+ self.__app.status_label = (err, ERRCOL)
self.logger.error(err)
cfg.set("sockhttps_b", 0)
self._chk_https.config(state=DISABLED)
@@ -541,7 +541,7 @@ def _set_advanced(self):
"""
if self._show_advanced:
- self._frm_advanced.grid(column=0, row=1, columnspan=5, sticky=(W, E))
+ self._frm_advanced.grid(column=0, row=1, columnspan=5, sticky=EW)
self._btn_toggle.config(image=self._img_contract)
else:
self._frm_advanced.grid_forget()
@@ -568,9 +568,9 @@ def _on_socketserve(self, var, index, mode):
"""
if self.valid_settings():
- self.__app.set_status("", INFOCOL)
+ self.__app.status_label = ("", INFOCOL)
else:
- self.__app.set_status("ERROR - invalid entry", ERRCOL)
+ self.__app.status_label = ("ERROR - invalid entry", ERRCOL)
return
self._quectel_restart = 0
@@ -650,9 +650,9 @@ def _config_receiver(self):
# validate settings
if self.valid_settings():
- self.__app.set_status("", INFOCOL)
+ self.__app.status_label = ("", INFOCOL)
else:
- self.__app.set_status("ERROR - invalid entry", ERRCOL)
+ self.__app.status_label = ("ERROR - invalid entry", ERRCOL)
return
delay = self.__app.configuration.get("guiupdateinterval_f") / 2
@@ -863,7 +863,7 @@ def _on_update_https(self, var, index, mode):
pem, pemexists = check_pemfile()
if self.https.get() and not pemexists:
err = DLGNOTLS.format(hostpem=pem)
- self.__app.set_status(err, ERRCOL)
+ self.__app.status_label = (err, ERRCOL)
self.logger.error(err)
self._attach_events(False)
self.https.set(0)
@@ -1020,7 +1020,7 @@ def clients(self, clients: int):
if self._socket_serve.get() in ("1", 1):
self.__app.frm_banner.update_transmit_status(clients)
- def _config_msg_rates(self, rate: int, port_type: str) -> UBXMessage:
+ def _config_msg_rates(self, rate: int, port_type: str):
"""
Configure RTCM3 and UBX NAV-SVIN message rates.
@@ -1144,12 +1144,12 @@ def svin_countdown(self, ela: int, valid: bool, active: bool):
self._pgb_elapsed.grid_forget()
@property
- def socketserving(self) -> bool:
+ def socketserving(self) -> int:
"""
Getter for socket serve flag.
:return: server running True/False
- :rtype: bool
+ :rtype: int
"""
return self._socket_serve.get()
diff --git a/src/pygpsclient/settings_frame.py b/src/pygpsclient/settings_frame.py
index c36867a3..7f7ed5dc 100644
--- a/src/pygpsclient/settings_frame.py
+++ b/src/pygpsclient/settings_frame.py
@@ -3,7 +3,8 @@
Settings frame class for PyGPSClient application.
-- Holds all the latest settings in self.config
+- Reads and updates configuration held in self.__app.configuration.
+- Starts or stops data logging.
- Sets initial (saved) configuration of the following frames:
- frm_settings (SettingsFrame class) for general application settings.
- frm_serial (SerialConfigFrame class) for serial port settings.
@@ -25,6 +26,7 @@
BOTH,
BOTTOM,
DISABLED,
+ EW,
HORIZONTAL,
LEFT,
NORMAL,
@@ -77,6 +79,7 @@
ICON_SOCKET,
ICON_TTYCONFIG,
ICON_UBXCONFIG,
+ INFOCOL,
KNOWNGPS,
MSGMODES,
NOPORTS,
@@ -438,8 +441,7 @@ def _body(self):
self._frm_options_btns,
width=45,
image=self._img_ubxconfig,
- command=lambda: self._on_ubx_config(),
- state=NORMAL,
+ command=lambda: self.__app.start_dialog(DLGTUBX),
)
self._lbl_nmeaconfig = Label(
self._frm_options_btns,
@@ -449,7 +451,7 @@ def _body(self):
self._frm_options_btns,
width=45,
image=self._img_nmeaconfig,
- command=lambda: self._on_nmea_config(),
+ command=lambda: self.__app.start_dialog(DLGTNMEA),
state=NORMAL,
)
self._lbl_ttyconfig = Label(
@@ -460,7 +462,7 @@ def _body(self):
self._frm_options_btns,
width=45,
image=self._img_ttyconfig,
- command=lambda: self._on_tty_config(),
+ command=lambda: self.__app.start_dialog(DLGTTTY),
state=NORMAL,
)
self._lbl_ntripconfig = Label(
@@ -471,7 +473,7 @@ def _body(self):
self._frm_options_btns,
width=45,
image=self._img_ntripconfig,
- command=lambda: self._on_ntrip_config(),
+ command=lambda: self.__app.start_dialog(DLGTNTRIP),
state=NORMAL,
)
# socket server configuration
@@ -485,36 +487,34 @@ def _do_layout(self):
Position widgets in frame.
"""
- self.frm_serial.grid(
- column=0, row=1, columnspan=4, padx=2, pady=2, sticky=(W, E)
- )
+ self.frm_serial.grid(column=0, row=1, columnspan=4, padx=2, pady=2, sticky=EW)
ttk.Separator(self._frm_container).grid(
- column=0, row=2, columnspan=4, padx=2, pady=2, sticky=(W, E)
+ column=0, row=2, columnspan=4, padx=2, pady=2, sticky=EW
)
self.frm_socketclient.grid(
- column=0, row=3, columnspan=4, padx=2, pady=2, sticky=(W, E)
+ column=0, row=3, columnspan=4, padx=2, pady=2, sticky=EW
)
ttk.Separator(self._frm_container).grid(
- column=0, row=4, columnspan=4, padx=2, pady=2, sticky=(W, E)
+ column=0, row=4, columnspan=4, padx=2, pady=2, sticky=EW
)
- self._frm_buttons.grid(column=0, row=5, columnspan=4, sticky=(W, E))
+ self._frm_buttons.grid(column=0, row=5, columnspan=4, sticky=EW)
self._btn_connect.grid(column=0, row=0, padx=2, pady=1)
self._btn_connect_socket.grid(column=1, row=0, padx=2, pady=1)
self._btn_connect_file.grid(column=2, row=0, padx=2, pady=1)
self._btn_disconnect.grid(column=3, row=0, padx=2, pady=1)
self._btn_exit.grid(column=4, row=0, padx=2, pady=1)
- self._lbl_connect.grid(column=0, row=1, padx=1, pady=1, sticky=(W, E))
- self._lbl_connect_socket.grid(column=1, row=1, padx=1, pady=1, sticky=(W, E))
- self._lbl_connect_file.grid(column=2, row=1, padx=1, pady=1, sticky=(W, E))
- self._lbl_disconnect.grid(column=3, row=1, padx=1, pady=1, sticky=(W, E))
+ self._lbl_connect.grid(column=0, row=1, padx=1, pady=1, sticky=EW)
+ self._lbl_connect_socket.grid(column=1, row=1, padx=1, pady=1, sticky=EW)
+ self._lbl_connect_file.grid(column=2, row=1, padx=1, pady=1, sticky=EW)
+ self._lbl_disconnect.grid(column=3, row=1, padx=1, pady=1, sticky=EW)
ttk.Separator(self._frm_container).grid(
- column=0, row=7, columnspan=4, padx=2, pady=2, sticky=(W, E)
+ column=0, row=7, columnspan=4, padx=2, pady=2, sticky=EW
)
- self._frm_options.grid(column=0, row=8, columnspan=4, sticky=(W, E))
+ self._frm_options.grid(column=0, row=8, columnspan=4, sticky=EW)
self._lbl_protocol.grid(column=0, row=0, padx=2, pady=2, sticky=W)
self._chk_nmea.grid(column=1, row=0, padx=0, pady=0, sticky=W)
self._chk_ubx.grid(column=2, row=0, padx=0, pady=0, sticky=W)
@@ -546,7 +546,7 @@ def _do_layout(self):
self._chk_recorddatabase.grid(
column=2, row=8, columnspan=2, padx=2, pady=2, sticky=W
)
- self._frm_options_btns.grid(column=0, row=9, columnspan=4, sticky=(W, E))
+ self._frm_options_btns.grid(column=0, row=9, columnspan=4, sticky=EW)
self._btn_ubxconfig.grid(column=0, row=0, padx=5)
self._lbl_ubxconfig.grid(column=0, row=1)
self._btn_nmeaconfig.grid(column=1, row=0, padx=5)
@@ -556,10 +556,10 @@ def _do_layout(self):
self._btn_ntripconfig.grid(column=3, row=0, padx=5)
self._lbl_ntripconfig.grid(column=3, row=1)
ttk.Separator(self._frm_container).grid(
- column=0, row=10, columnspan=4, padx=2, pady=2, sticky=(W, E)
+ column=0, row=10, columnspan=4, padx=2, pady=2, sticky=EW
)
self.frm_socketserver.grid(
- column=0, row=11, columnspan=4, padx=2, pady=2, sticky=(W, E)
+ column=0, row=11, columnspan=4, padx=2, pady=2, sticky=EW
)
def _attach_events(self, add: bool = True):
@@ -782,34 +782,6 @@ def _on_update_logformat(self, var, index, mode):
self.__app.configuration.set("logformat_s", self._logformat.get())
- def _on_ubx_config(self, *args, **kwargs):
- """
- Open UBX configuration dialog panel.
- """
-
- self.__app.start_dialog(DLGTUBX)
-
- def _on_nmea_config(self, *args, **kwargs):
- """
- Open NMEA configuration dialog panel.
- """
-
- self.__app.start_dialog(DLGTNMEA)
-
- def _on_tty_config(self, *args, **kwargs):
- """
- Open TTY configuration dialog panel.
- """
-
- self.__app.start_dialog(DLGTTTY)
-
- def _on_ntrip_config(self, *args, **kwargs):
- """
- Open NTRIP Client configuration dialog panel.
- """
-
- self.__app.start_dialog(DLGTNTRIP)
-
def _on_data_log(self, var, index, mode):
"""
Start or stop data logger.
@@ -821,7 +793,10 @@ def _on_data_log(self, var, index, mode):
if self.logpath is not None:
self.__app.configuration.set("datalog_b", 1)
self.__app.configuration.set("logpath_s", self.logpath)
- self.__app.set_status(f"Data logging enabled: {self.logpath}")
+ self.__app.status_label = (
+ f"Data logging enabled: {self.logpath}",
+ INFOCOL,
+ )
if not self.__app.file_handler.open_logfile():
self.logpath = ""
self._datalog.set(0)
@@ -833,7 +808,7 @@ def _on_data_log(self, var, index, mode):
self.__app.configuration.set("datalog_b", 0)
self._datalog.set(0)
self.__app.file_handler.close_logfile()
- self.__app.set_status("Data logging disabled")
+ self.__app.status_label = ("Data logging disabled", INFOCOL)
self._spn_datalog.config(state=READONLY)
def _on_record_track(self, var, index, mode):
@@ -847,7 +822,7 @@ def _on_record_track(self, var, index, mode):
if self.trackpath is not None:
self.__app.configuration.set("recordtrack_b", 1)
self.__app.configuration.set("trackpath_s", self.trackpath)
- self.__app.set_status(f"Track recording enabled: {self.trackpath}")
+ self.__app.status_label = f"Track recording enabled: {self.trackpath}"
if not self.__app.file_handler.open_trackfile():
self.trackpath = ""
self._record_track.set(0)
@@ -858,7 +833,7 @@ def _on_record_track(self, var, index, mode):
self._record_track.set(0)
self.__app.configuration.set("recordtrack_b", 0)
self.__app.file_handler.close_trackfile()
- self.__app.set_status("Track recording disabled")
+ self.__app.status_label = "Track recording disabled"
def _on_record_database(self, var, index, mode):
"""
@@ -877,7 +852,7 @@ def _on_record_database(self, var, index, mode):
self._record_database.set(0)
else:
self.__app.configuration.set("database_b", 0)
- self.__app.set_status("Database recording disabled")
+ self.__app.status_label = "Database recording disabled"
def _on_connect(self, conntype: int):
"""
@@ -901,7 +876,7 @@ def _on_connect(self, conntype: int):
"inactivity_timeout": self.frm_serial.inactivity_timeout,
}
- self.frm_socketserver.set_status(conntype)
+ self.frm_socketserver.status_label = conntype
if conntype == CONNECTED:
frm = self.frm_serial
if frm.status == NOPORTS:
@@ -913,7 +888,7 @@ def _on_connect(self, conntype: int):
elif conntype == CONNECTED_SOCKET:
frm = self.frm_socketclient
if not frm.valid_settings():
- self.__app.set_status("ERROR - invalid settings", ERRCOL)
+ self.__app.status_label = ("ERROR - invalid settings", ERRCOL)
return
connstr = f"{frm.server.get()}:{frm.port.get()}"
conndict = dict(conndict, **{"socket_settings": frm})
@@ -921,6 +896,7 @@ def _on_connect(self, conntype: int):
self.__app.poll_version(conndict["protocol"])
elif conntype == CONNECTED_FILE:
self.infilepath = self.__app.file_handler.open_file(
+ self,
"datalog",
(
("datalog files", "*.log"),
@@ -940,9 +916,9 @@ def _on_connect(self, conntype: int):
else:
return
- self.__app.set_connection(connstr, OKCOL)
- self.__app.set_status("")
self.__app.conn_status = conntype
+ self.__app.conn_label = (connstr, OKCOL)
+ self.__app.status_label = ("", INFOCOL)
self._reset_frames()
self.__app.stream_handler.start(self.__app, conndict)
@@ -957,9 +933,9 @@ def enable_controls(self, status: int):
"""
- self.frm_serial.set_status(status)
- self.frm_socketclient.set_status(status)
- self.frm_socketserver.set_status(status)
+ self.frm_serial.status_label = status
+ self.frm_socketclient.status_label = status
+ self.frm_socketserver.status_label = status
self._btn_connect.config(
state=(
diff --git a/src/pygpsclient/skyview_frame.py b/src/pygpsclient/skyview_frame.py
index 6d2bd5fb..807be43a 100644
--- a/src/pygpsclient/skyview_frame.py
+++ b/src/pygpsclient/skyview_frame.py
@@ -14,8 +14,7 @@
# pylint: disable = no-member
-from operator import itemgetter
-from tkinter import BOTH, YES, Frame
+from tkinter import NSEW, Frame
from pygpsclient.canvas_plot import (
MODE_CEL,
@@ -30,7 +29,7 @@
GNSS_LIST,
WIDGETU2,
)
-from pygpsclient.helpers import col2contrast, snr2col
+from pygpsclient.helpers import col2contrast, snr2col, unused_sats
OL_WID = 4
FONTSCALE = 30
@@ -79,7 +78,7 @@ def _body(self):
height=self.height,
bg=self.bg_col,
)
- self._canvas.pack(fill=BOTH, expand=YES)
+ self._canvas.grid(column=0, row=0, sticky=NSEW)
def _attach_events(self):
"""
@@ -107,16 +106,23 @@ def update_frame(self):
"""
data = self.__app.gnss_status.gsv_data
+ show_unused = self.__app.configuration.get("unusedsat_b")
+ siv = len(data)
+ if siv == 0:
+ return
+ siv = siv if show_unused else siv - unused_sats(data)
+
self.init_frame()
- for d in sorted(data.values(), key=itemgetter(4)): # sort by ascending snr
+ for val in sorted(data.values(), key=lambda x: x[4]): # sort by ascending C/N0
try:
- gnssId, prn, ele, azi, snr = d
+ gnssId, prn, ele, azi, cno, _ = val
+ if cno == 0 and not show_unused:
+ continue
x, y = self._canvas.d2xy(int(azi), int(ele))
- snr = 0 if snr == "" else int(snr)
(_, ol_col) = GNSS_LIST[gnssId]
prn = f"{int(prn):02}"
- bg_col = snr2col(snr)
+ bg_col = snr2col(cno)
self._canvas.create_circle(
x,
y,
@@ -137,7 +143,7 @@ def update_frame(self):
except ValueError:
pass
- self._canvas.update_idletasks()
+ self.update_idletasks()
def _on_resize(self, event): # pylint: disable=unused-argument
"""
diff --git a/src/pygpsclient/socketconfig_frame.py b/src/pygpsclient/socketconfig_frame.py
index bb546440..7af72f35 100644
--- a/src/pygpsclient/socketconfig_frame.py
+++ b/src/pygpsclient/socketconfig_frame.py
@@ -18,9 +18,9 @@
from tkinter import (
DISABLED,
+ EW,
NORMAL,
Checkbutton,
- E,
Entry,
Frame,
IntVar,
@@ -91,7 +91,6 @@ def __init__(self, app, container, *args, **kwargs):
self._do_layout()
self.reset()
# self._attach_events() # done in reset
- self._attach_events1()
def _body(self):
"""
@@ -133,11 +132,9 @@ def _do_layout(self):
Layout widgets.
"""
- self._frm_basic.grid(column=0, row=0, columnspan=4, sticky=(W, E))
+ self._frm_basic.grid(column=0, row=0, columnspan=4, sticky=EW)
self._lbl_server.grid(column=0, row=0, padx=2, pady=2, sticky=W)
- self.ent_server.grid(
- column=1, row=0, padx=2, pady=2, columnspan=4, sticky=(W, E)
- )
+ self.ent_server.grid(column=1, row=0, padx=2, pady=2, columnspan=4, sticky=EW)
self._lbl_port.grid(column=0, row=1, padx=2, pady=2, sticky=W)
self.ent_port.grid(column=1, row=1, padx=2, pady=2, sticky=W)
self._lbl_protocol.grid(column=2, row=1, padx=2, pady=2, sticky=W)
@@ -145,13 +142,6 @@ def _do_layout(self):
self._chk_https.grid(column=1, row=2, padx=2, pady=2, sticky=W)
self._chk_selfsign.grid(column=2, row=2, padx=2, pady=2, sticky=W)
- def _attach_events1(self):
- """
- Bind resize event to frame.
- """
-
- self.bind("", self._on_resize)
-
def _attach_events(self, add: bool = True):
"""
Add or remove event bindings to/from widgets.
diff --git a/src/pygpsclient/spartn_dialog.py b/src/pygpsclient/spartn_dialog.py
index 29a513dc..c37b6ab9 100644
--- a/src/pygpsclient/spartn_dialog.py
+++ b/src/pygpsclient/spartn_dialog.py
@@ -16,7 +16,8 @@
:license: BSD 3-Clause
"""
-from tkinter import E, N, S, W
+from tkinter import NSEW
+from types import NoneType
from pyubx2 import UBXMessage
@@ -100,7 +101,7 @@ def _do_layout(self):
row=0,
ipadx=5,
ipady=5,
- sticky=(N, S, W, E),
+ sticky=NSEW,
)
col = 1
if self._lband_enabled:
@@ -109,7 +110,7 @@ def _do_layout(self):
row=0,
ipadx=5,
ipady=5,
- sticky=(N, S, W, E),
+ sticky=NSEW,
)
col += 1
self.frm_gnss.grid(
@@ -117,7 +118,7 @@ def _do_layout(self):
row=0,
ipadx=5,
ipady=5,
- sticky=(N, S, W, E),
+ sticky=NSEW,
)
def _reset(self):
@@ -125,7 +126,7 @@ def _reset(self):
Reset configuration widgets.
"""
- self.set_status("")
+ self.status_label = ""
def _attach_events(self):
"""
@@ -134,18 +135,6 @@ def _attach_events(self):
# self.bind("", self._on_resize)
- def set_status(self, message: str, color: str = ""):
- """
- Set status message.
- :param str message: message to be displayed
- :param str color: rgb color of text
- """
-
- message = (message[:180] + "..") if len(message) > 180 else message
- if color != "":
- self._lbl_status.config(fg=color)
- self._status.set(" " + message)
-
def set_pending(self, msgid: int, spartnfrm: int):
"""
Set pending confirmation flag for UBX configuration frame to
@@ -186,12 +175,12 @@ def update_pending(self, msg: UBXMessage):
if self._pending_confs.get(msgid, None) == spartnfrm:
self._pending_confs.pop(msgid)
- def set_controls(self, status: bool, msgt: tuple = None):
+ def set_controls(self, status: bool, msgt: tuple | NoneType = None):
"""
Set controls in IP or L-Band clients.
:param bool status: connected to SPARTN server yes/no
- :param tuple msgt: tuple of (message, color)
+ :param tuple | Nonetype msgt: tuple of (message, color) or None
"""
if status == CONNECTED_SPARTNIP:
@@ -199,8 +188,7 @@ def set_controls(self, status: bool, msgt: tuple = None):
elif status == CONNECTED_SPARTNLB:
self.frm_corrlband.set_controls(status)
if msgt is not None:
- msg, col = msgt
- self.set_status(msg, col)
+ self.status_label = msgt
def disconnect_ip(self, msg: str = ""):
"""
diff --git a/src/pygpsclient/spartn_gnss_frame.py b/src/pygpsclient/spartn_gnss_frame.py
index dc6b5273..af5c8388 100644
--- a/src/pygpsclient/spartn_gnss_frame.py
+++ b/src/pygpsclient/spartn_gnss_frame.py
@@ -19,10 +19,10 @@
from datetime import datetime, timedelta, timezone
from tkinter import (
+ EW,
NORMAL,
Button,
Checkbutton,
- E,
Entry,
Frame,
IntVar,
@@ -60,6 +60,7 @@
VALHEX,
VALLEN,
)
+from pygpsclient.helpers import validate # pylint: disable=unused-import
from pygpsclient.helpers import (
date2wnotow,
parse_rxmspartnkey,
@@ -249,7 +250,7 @@ def _do_layout(self):
self._lbl_loadjson.grid(column=0, row=1, padx=3, pady=2, sticky=W)
self._btn_loadjson.grid(column=1, row=1, columnspan=3, padx=3, pady=2, sticky=W)
ttk.Separator(self).grid(
- column=0, row=2, columnspan=4, padx=2, pady=3, sticky=(W, E)
+ column=0, row=2, columnspan=4, padx=2, pady=3, sticky=EW
)
self._lbl_curr.grid(column=0, row=3, columnspan=4, padx=3, pady=2, sticky=W)
self._lbl_key1.grid(column=0, row=4, padx=3, pady=2, sticky=W)
@@ -257,7 +258,7 @@ def _do_layout(self):
self._lbl_valdate1.grid(column=0, row=5, padx=3, pady=2, sticky=W)
self._ent_valdate1.grid(column=1, row=5, columnspan=3, padx=3, pady=2, sticky=W)
ttk.Separator(self).grid(
- column=0, row=6, columnspan=4, padx=2, pady=3, sticky=(W, E)
+ column=0, row=6, columnspan=4, padx=2, pady=3, sticky=EW
)
self._lbl_next.grid(column=0, row=7, columnspan=3, padx=3, pady=2, sticky=W)
self._lbl_key2.grid(column=0, row=8, padx=3, pady=2, sticky=W)
@@ -265,7 +266,7 @@ def _do_layout(self):
self._lbl_valdate2.grid(column=0, row=9, padx=3, pady=2, sticky=W)
self._ent_valdate2.grid(column=1, row=9, columnspan=3, padx=3, pady=2, sticky=W)
ttk.Separator(self).grid(
- column=0, row=10, columnspan=4, padx=2, pady=3, sticky=(W, E)
+ column=0, row=10, columnspan=4, padx=2, pady=3, sticky=EW
)
self._rad_source0.grid(column=0, row=11, padx=3, pady=2, sticky=W)
self._rad_source1.grid(column=1, row=11, columnspan=3, padx=3, pady=2, sticky=W)
@@ -277,7 +278,7 @@ def _do_layout(self):
column=2, row=13, columnspan=2, padx=3, pady=2, sticky=W
)
ttk.Separator(self).grid(
- column=0, row=14, columnspan=4, padx=2, pady=3, sticky=(W, E)
+ column=0, row=14, columnspan=4, padx=2, pady=3, sticky=EW
)
self._btn_send_command.grid(column=2, row=15, padx=3, pady=2, sticky=W)
self._lbl_send_command.grid(column=3, row=15, padx=3, pady=2, sticky=W)
@@ -340,7 +341,7 @@ def _valid_gnss_settings(self) -> bool:
valid = valid & self._ent_valdate2.validate(VALDMY)
if not valid:
- self.__container.set_status("ERROR - invalid settings", ERRCOL)
+ self.__container.status_label = ("ERROR - invalid settings", ERRCOL)
return valid
@@ -423,8 +424,7 @@ def _on_send_gnss_config(self):
return
if self.__app.conn_status != CONNECTED:
- self.__container.set_status(NOTCONN, ERRCOL)
- # return
+ self.__container.status_label = (NOTCONN, ERRCOL)
if self._send_f9p_config.get():
msgc = "config"
@@ -442,13 +442,13 @@ def _on_send_gnss_config(self):
else:
msgk = ""
if msgk == "" and msgc == "":
- self.__container.set_status(NULLSEND, ERRCOL)
+ self.__container.status_label = (NULLSEND, ERRCOL)
return
if msgk != "" and msgc != "":
msga = " and "
else:
msga = ""
- self.__container.set_status(f"{(msgk + msga + msgc).capitalize()} sent", OKCOL)
+ self.__container.status_label = f"{(msgk + msga + msgc).capitalize()} sent"
self._lbl_send_command.config(image=self._img_pending)
for msgid in ("RXM-SPARTNKEY", "CFG-VALGET", "ACK-ACK", "ACK-NAK"):
self.__container.set_pending(msgid, SPARTN_GNSS)
@@ -503,13 +503,13 @@ def update_status(self, msg: UBXMessage):
col = OKCOL
else:
col = ERRCOL
- self.__container.set_status(CONFIGRXM.format(RXMMSG, msg.numKeys), col)
+ self.__container.status_label = (CONFIGRXM.format(RXMMSG, msg.numKeys), col)
elif msg.identity == "ACK-ACK":
self._lbl_send_command.config(image=self._img_confirmed)
- self.__container.set_status(CONFIGOK.format(CFGSET), OKCOL)
+ self.__container.status_label = (CONFIGOK.format(CFGSET), OKCOL)
elif msg.identity == "ACK-NAK":
self._lbl_send_command.config(image=self._img_warn)
- self.__container.set_status(CONFIGBAD.format(CFGSET), ERRCOL)
+ self.__container.status_label = (CONFIGBAD.format(CFGSET), ERRCOL)
self.update_idletasks()
def _on_load_json(self):
@@ -519,7 +519,7 @@ def _on_load_json(self):
# pylint: disable=unused-variable
jsonfile = self.__app.file_handler.open_file(
- "spartnjson", (("json files", "*.json"), ("all files", "*.*"))
+ self, "spartnjson", (("json files", "*.json"), ("all files", "*.*"))
)
if jsonfile is None:
return
@@ -536,6 +536,6 @@ def _on_load_json(self):
(key, start, _) = spc.next_key
self._spartn_key2.set(key)
self._spartn_valdate2.set(start.strftime("%Y%m%d"))
- self.__container.set_status(DLGJSONOK.format(jsonfile), OKCOL)
+ self.__container.status_label = (DLGJSONOK.format(jsonfile), OKCOL)
except Exception as err: # pylint: disable=broad-exception-caught
- self.__container.set_status(DLGJSONERR.format(err), ERRCOL)
+ self.__container.status_label = (DLGJSONERR.format(err), ERRCOL)
diff --git a/src/pygpsclient/spartn_lband_frame.py b/src/pygpsclient/spartn_lband_frame.py
index 589f0fee..134779cf 100644
--- a/src/pygpsclient/spartn_lband_frame.py
+++ b/src/pygpsclient/spartn_lband_frame.py
@@ -17,16 +17,15 @@
from tkinter import (
DISABLED,
+ EW,
NORMAL,
+ NSEW,
Button,
Checkbutton,
- E,
Entry,
Frame,
IntVar,
Label,
- N,
- S,
Spinbox,
StringVar,
TclError,
@@ -67,6 +66,7 @@
TRACEMODE_WRITE,
VALINT,
)
+from pygpsclient.helpers import validate # pylint: disable=unused-import
from pygpsclient.serialconfig_lband_frame import SerialConfigLbandFrame
from pygpsclient.strings import CONFIGBAD, CONFIGOK, DLGSPARTNWARN, LBLSPARTNLB
@@ -310,10 +310,10 @@ def _do_layout(self):
column=0, row=0, columnspan=4, padx=3, pady=2, sticky=W
)
self._frm_spartn_serial.grid(
- column=0, row=1, columnspan=4, padx=3, pady=2, sticky=(N, S, W, E)
+ column=0, row=1, columnspan=4, padx=3, pady=2, sticky=NSEW
)
ttk.Separator(self).grid(
- column=0, row=2, columnspan=4, padx=2, pady=3, sticky=(W, E)
+ column=0, row=2, columnspan=4, padx=2, pady=3, sticky=EW
)
self._lbl_freq.grid(column=0, row=3, sticky=W)
self._ent_freq.grid(column=1, row=3, sticky=W)
@@ -337,7 +337,7 @@ def _do_layout(self):
self._lbl_ebno.grid(column=0, row=11, sticky=W)
self._lbl_fec.grid(column=1, row=11, sticky=W)
ttk.Separator(self).grid(
- column=0, row=12, columnspan=4, padx=2, pady=3, sticky=(W, E)
+ column=0, row=12, columnspan=4, padx=2, pady=3, sticky=EW
)
self._btn_connect.grid(column=0, row=13, padx=3, pady=2, sticky=W)
self._btn_disconnect.grid(column=1, row=13, padx=3, pady=2, sticky=W)
@@ -383,7 +383,7 @@ def _reset(self):
self._spartn_descrminit.set(cfg.get("lbandclientdescrminit_n"))
self._spartn_unqword.set(cfg.get("lbandclientunqword_s"))
self._spartn_outport.set(cfg.get("lbandclientoutport_s"))
- self.__container.set_status("")
+ self.__container.status_label = ""
if self.__app.rtk_conn_status == CONNECTED_SPARTNLB:
self.set_controls(CONNECTED_SPARTNLB)
else:
@@ -418,7 +418,7 @@ def set_controls(self, status: int):
:param int status: connection status (0 = disconnected, 1 = connected)
"""
- self._frm_spartn_serial.set_status(status)
+ self._frm_spartn_serial.status_label = status
stat = DISABLED if status == CONNECTED_SPARTNLB else NORMAL
for wdg in (self._btn_connect,):
wdg.config(state=stat)
@@ -461,7 +461,7 @@ def _valid_settings(self) -> bool:
valid = valid & self._ent_unqword.validate(VALINT, 0, U8MAX) # U8
if not valid:
- self.__container.set_status("ERROR - invalid settings", ERRCOL)
+ self.__container.status_label = ("ERROR - invalid settings", ERRCOL)
return valid
@@ -472,7 +472,7 @@ def on_connect(self):
frm = self._frm_spartn_serial
if self.__app.rtk_conn_status == CONNECTED_SPARTNIP:
- self.__container.set_status(
+ self.__container.status_label = (
DLGSPARTNWARN.format("IP", "L-Band"),
ERRCOL,
)
@@ -497,8 +497,8 @@ def on_connect(self):
# start serial stream thread
self.__app.rtk_conn_status = CONNECTED_SPARTNLB
self.__app.spartn_stream_handler.start(self.__container, conndict)
- self.__container.set_status(
- f"Connected to {frm.port}:{frm.port_desc} @ {frm.bpsrate}", OKCOL
+ self.__container.status_label = (
+ f"Connected to {frm.port}:{frm.port_desc} @ {frm.bpsrate}"
)
self.set_controls(CONNECTED_SPARTNLB)
@@ -516,10 +516,7 @@ def on_disconnect(self, msg: str = ""):
if self.__app.spartn_stream_handler is not None:
self.__app.spartn_stream_handler.stop()
self.__app.rtk_conn_status = DISCONNECTED
- self.__container.set_status(
- msg,
- ERRCOL,
- )
+ self.__container.status_label = (msg, ERRCOL)
self.set_controls(DISCONNECTED)
def on_error(self, event): # pylint: disable=unused-argument
@@ -627,7 +624,7 @@ def _on_send_config(self):
msg = self._format_cfgcorr()
self._send_command(msg)
- self.__container.set_status(f"{CFGSET} command sent", OKCOL)
+ self.__container.status_label = f"{CFGSET} command sent"
self._lbl_send.config(image=self._img_pending)
# save config to persistent memory
@@ -680,14 +677,14 @@ def update_status(self, msg: UBXMessage):
self._spartn_prescrm.set(msg.CFG_PMP_USE_PRESCRAMBLING)
if hasattr(msg, "CFG_PMP_UNIQUE_WORD"):
self._spartn_unqword.set(msg.CFG_PMP_UNIQUE_WORD)
- self.__container.set_status(f"{CFGPOLL} received", OKCOL)
+ self.__container.status_label = (f"{CFGPOLL} received", OKCOL)
self._lbl_send.config(image=self._img_confirmed)
elif msg.identity == "ACK-ACK":
self._lbl_send.config(image=self._img_confirmed)
- self.__container.set_status(CONFIGOK.format(CFGSET), OKCOL)
+ self.__container.status_label = (CONFIGOK.format(CFGSET), OKCOL)
elif msg.identity == "ACK-NAK":
self._lbl_send.config(image=self._img_warn)
- self.__container.set_status(CONFIGBAD.format(CFGSET), ERRCOL)
+ self.__container.status_label = (CONFIGBAD.format(CFGSET), ERRCOL)
elif msg.identity == "RXM-PMP":
self._lbl_ebno.config(text=f"Eb/N0: {msg.ebno} dB")
self._lbl_fec.config(text=f"FEC Bits: {msg.fecBits}")
diff --git a/src/pygpsclient/spartn_mqtt_frame.py b/src/pygpsclient/spartn_mqtt_frame.py
index 069e52b5..2ebcc79c 100644
--- a/src/pygpsclient/spartn_mqtt_frame.py
+++ b/src/pygpsclient/spartn_mqtt_frame.py
@@ -22,6 +22,7 @@
from pathlib import Path
from tkinter import (
DISABLED,
+ EW,
NORMAL,
Button,
Checkbutton,
@@ -76,7 +77,7 @@
VALINT,
VALLEN,
)
-from pygpsclient.helpers import MAXPORT
+from pygpsclient.helpers import MAXPORT, validate # pylint: disable=unused-import
from pygpsclient.strings import DLGSPARTNWARN, LBLSPARTNIP, MQTTCONN
@@ -279,19 +280,15 @@ def _do_layout(self):
self._chk_mqtt_keytopic.grid(column=3, row=5, padx=3, pady=2, sticky=W)
self._chk_mqtt_freqtopic.grid(column=4, row=5, padx=3, pady=2, sticky=W)
self._lbl_mqttcrt.grid(column=0, row=6, padx=3, pady=2, sticky=W)
- self._ent_mqttcrt.grid(
- column=1, row=6, padx=3, columnspan=3, pady=2, sticky=(W, E)
- )
+ self._ent_mqttcrt.grid(column=1, row=6, padx=3, columnspan=3, pady=2, sticky=EW)
self._btn_opencrt.grid(column=4, row=6, padx=3, pady=2, sticky=E)
self._lbl_mqttpem.grid(column=0, row=7, padx=3, pady=2, sticky=W)
- self._ent_mqttpem.grid(
- column=1, row=7, columnspan=3, padx=3, pady=2, sticky=(W, E)
- )
+ self._ent_mqttpem.grid(column=1, row=7, columnspan=3, padx=3, pady=2, sticky=EW)
self._btn_openpem.grid(column=4, row=7, padx=3, pady=2, sticky=E)
self._lbl_spartndecode.grid(column=0, row=8, columnspan=2, sticky=W)
self._chk_spartndecode.grid(column=2, row=8, sticky=W)
ttk.Separator(self).grid(
- column=0, row=9, columnspan=6, padx=2, pady=3, sticky=(W, E)
+ column=0, row=9, columnspan=6, padx=2, pady=3, sticky=EW
)
self._btn_connect.grid(column=0, row=10, padx=3, pady=2, sticky=W)
self._btn_disconnect.grid(column=2, row=10, padx=3, pady=2, sticky=W)
@@ -324,9 +321,7 @@ def _reset(self):
self._get_settings()
self._reset_keypaths(self._mqtt_clientid.get())
- self.__container.set_status(
- "",
- )
+ self.__container.status_label = ""
if self.__app.rtk_conn_status == CONNECTED_SPARTNIP:
self.set_controls(CONNECTED_SPARTNIP)
else:
@@ -519,6 +514,7 @@ def _get_spartncerts(self, ext: str):
"""
spfile = self.__app.file_handler.open_file(
+ self,
"spartncert",
(
("spartn files", f"*.{ext}"),
@@ -536,7 +532,7 @@ def on_connect(self):
"""
if self.__app.rtk_conn_status == CONNECTED_SPARTNLB:
- self.__container.set_status(
+ self.__container.status_label = (
DLGSPARTNWARN.format("L-Band", "IP"),
ERRCOL,
)
@@ -571,13 +567,10 @@ def on_connect(self):
output=self._settings["output"],
)
self.set_controls(CONNECTED_SPARTNIP)
- self.__container.set_status(
- MQTTCONN.format(server),
- OKCOL,
- )
+ self.__container.status_label = (MQTTCONN.format(server), OKCOL)
self.__app.rtk_conn_status = CONNECTED_SPARTNIP
else:
- self.__container.set_status("ERROR! Invalid Settings", ERRCOL)
+ self.__container.status_label = ("ERROR! Invalid Settings", ERRCOL)
def on_disconnect(self, msg: str = ""):
"""
@@ -590,10 +583,7 @@ def on_disconnect(self, msg: str = ""):
if self.__app.rtk_conn_status == CONNECTED_SPARTNIP:
self.__app.spartn_handler.stop()
self.__app.rtk_conn_status = DISCONNECTED
- self.__container.set_status(
- msg,
- ERRCOL,
- )
+ self.__container.status_label = (msg, ERRCOL)
self.set_controls(DISCONNECTED)
def update_status(self, msg: UBXMessage):
diff --git a/src/pygpsclient/spectrum_frame.py b/src/pygpsclient/spectrum_frame.py
index c4aea6b2..60196b66 100644
--- a/src/pygpsclient/spectrum_frame.py
+++ b/src/pygpsclient/spectrum_frame.py
@@ -16,7 +16,7 @@
# pylint: disable=no-member, unused-argument
import logging
-from tkinter import ALL, NW, Checkbutton, E, Frame, IntVar, N, S, W
+from tkinter import ALL, EW, NSEW, NW, Checkbutton, Frame, IntVar, S, W
from pyubx2 import UBXMessage
@@ -143,8 +143,8 @@ def _body(self):
variable=self._pgaoffset,
anchor=W,
)
- self._canvas.grid(column=0, row=0, columnspan=3, sticky=(N, S, E, W))
- self.chk_pgaoffset.grid(column=0, row=1, sticky=(W, E))
+ self._canvas.grid(column=0, row=0, columnspan=3, sticky=NSEW)
+ self.chk_pgaoffset.grid(column=0, row=1, sticky=EW)
def _attach_events(self):
"""
@@ -333,6 +333,7 @@ def _update_plot(self, rfblocks: list, mode: str = MODELIVE, colors: dict = None
width=OL_WID,
tags=(mode, TAG_DATA),
)
+ self.update_idletasks()
# display any marked db/hz coordinate
if self._chartpos is not None:
diff --git a/src/pygpsclient/sqlite_handler.py b/src/pygpsclient/sqlite_handler.py
index e5933167..a9bea021 100644
--- a/src/pygpsclient/sqlite_handler.py
+++ b/src/pygpsclient/sqlite_handler.py
@@ -26,6 +26,7 @@
import traceback
from datetime import datetime, timezone
from os import path
+from types import NoneType
from pynmeagps import ecef2llh
@@ -127,8 +128,9 @@ def _create(
"""
try:
- self.__app.set_status(
- f"Database {self._db} initialising - please wait...", INFOCOL
+ self.__app.status_label = (
+ f"Database {self._db} initialising - please wait...",
+ INFOCOL,
)
self.logger.debug("Spatial metadata initialisation in progress...")
self._connection.execute("SELECT InitSpatialMetaData();")
@@ -137,7 +139,10 @@ def _create(
self._cursor.executescript(SQLC1.format(table=tbname))
return SQLOK
except sqlite3.Error as err:
- self.__app.set_status(f"Error initialising spatial database {err}", ERRCOL)
+ self.__app.status_label = (
+ f"Error initialising spatial database {err}",
+ ERRCOL,
+ )
self.logger.debug(traceback.format_exc())
return SQLERR
@@ -146,7 +151,7 @@ def open(
dbpath: str = HOME,
dbname: str = DBNAME,
tbname: str = TBNAME,
- ) -> str:
+ ) -> str | int:
"""
Create sqlite3 connection and cursor.
@@ -154,7 +159,7 @@ def open(
:param str dbname: name of sqlite3 database file
:param str tbname: name of table containing gnss data
:return: result
- :rtype: str
+ :rtype: str | int
"""
testing = dbname == DBINMEM
@@ -181,18 +186,18 @@ def open(
if testing:
self._connection.close()
else:
- self.__app.set_status(f"Database {self._db} opened", OKCOL)
+ self.__app.status_label = (f"Database {self._db} opened", OKCOL)
return SQLOK
except AttributeError as err:
- self.__app.set_status(f"SQL error: {err}", errcol)
+ self.__app.status_label = (f"SQL error: {err}", errcol)
self.logger.debug(traceback.format_exc())
return NOEXT # extensions not supported
except sqlite3.OperationalError as err:
- self.__app.set_status(f"SQL error {db}: {err}", errcol)
+ self.__app.status_label = (f"SQL error {db}: {err}", errcol)
self.logger.debug(traceback.format_exc())
return NOMODS # no mod_spatial extension found
except sqlite3.Error as err:
- self.__app.set_status(f"SQL error {db}: {err}", errcol)
+ self.__app.status_label = (f"SQL error {db}: {err}", errcol)
self.logger.debug(traceback.format_exc())
return SQLERR # other sqlite error
@@ -257,17 +262,17 @@ def load_data(self, ignore_null: bool = True) -> str:
self.logger.debug(f"Executed SQL statement {sql}")
return SQLOK
except sqlite3.Error as err:
- self.__app.set_status(f"SQL error: {err}", ERRCOL)
+ self.__app.status_label = (f"SQL error: {err}", ERRCOL)
self.logger.debug(traceback.format_exc())
return SQLERR
@property
- def database(self) -> str:
+ def database(self) -> str | NoneType:
"""
Getter for database name.
- :return: database path/name
- :rtype: str
+ :return: database path/name or None
+ :rtype: str | NoneType
"""
return self._db
diff --git a/src/pygpsclient/status_frame.py b/src/pygpsclient/status_frame.py
index ac9fb290..9830d10e 100644
--- a/src/pygpsclient/status_frame.py
+++ b/src/pygpsclient/status_frame.py
@@ -12,7 +12,7 @@
:license: BSD 3-Clause
"""
-from tkinter import VERTICAL, E, Frame, Label, N, S, StringVar, W, ttk
+from tkinter import EW, NS, VERTICAL, Frame, Label, W, ttk
class StatusFrame(Frame):
@@ -33,8 +33,6 @@ def __init__(self, app, *args, **kwargs):
self.__master = self.__app.appmaster # Reference to root class (Tk)
Frame.__init__(self, self.__master, *args, **kwargs)
- self._status = StringVar()
- self._connection = StringVar()
self.width, self.height = self.get_size()
self._body()
@@ -49,49 +47,11 @@ def _body(self):
self.option_add("*Font", self.__app.font_md)
- self._lbl_connection = Label(self, textvariable=self._connection, anchor=W)
- self._lbl_status_preset = Label(self, textvariable=self._status, anchor=W)
- self._lbl_connection.grid(column=0, row=0, sticky=(W, E))
- ttk.Separator(self, orient=VERTICAL).grid(column=1, row=0, sticky=(N, S))
- self._lbl_status_preset.grid(column=2, row=0, sticky=(W, E))
-
- def set_connection(self, connection: str, color: str = ""):
- """
- Sets connection description in status bar.
-
- :param str connection: description of connection
- :param str color: rgb color string (default=blue)
-
- """
-
- if len(connection) > 100:
- connection = "..." + connection[-100:]
- if color != "":
- self._lbl_connection.config(fg=color)
- self._connection.set(" " + connection)
-
- def set_status(self, message, color: str = ""):
- """
- Sets message in status bar.
-
- :param str message: message to be displayed in status bar
- :param str color: rgb color string (default=blue)
-
- """
-
- if len(message) > 200:
- message = "..." + message[-200:]
- if color != "":
- self._lbl_status_preset.config(fg=color)
- self._status.set(" " + message)
-
- def clear_status(self):
- """
- Clears status bar.
- """
-
- self._connection.set("")
- self._status.set("")
+ self.lbl_connection = Label(self, anchor=W)
+ self.lbl_status = Label(self, anchor=W)
+ self.lbl_connection.grid(column=0, row=0, sticky=EW)
+ ttk.Separator(self, orient=VERTICAL).grid(column=1, row=0, sticky=NS)
+ self.lbl_status.grid(column=2, row=0, sticky=EW)
def _on_resize(self, event): # pylint: disable=unused-argument
"""
@@ -112,6 +72,4 @@ def get_size(self):
"""
self.update_idletasks() # Make sure we know about any resizing
- width = self.winfo_width()
- height = self.winfo_height()
- return (width, height)
+ return self.winfo_width(), self.winfo_height()
diff --git a/src/pygpsclient/stream_handler.py b/src/pygpsclient/stream_handler.py
index 1a185e7d..eb4dec2d 100644
--- a/src/pygpsclient/stream_handler.py
+++ b/src/pygpsclient/stream_handler.py
@@ -16,7 +16,7 @@ class to read and parse incoming data from the receiver. It places
- SettingsFrame - i/o with the main GNSS receiver.
- SpartnLbandDialog - i/o with a SPARTN L-Band receiver when SPARTN Client active.
-The caller object can implement a 'set_status()' method to
+The caller object can implement a 'status_label = ()' method to
display any status messages output by StreamHandler.
Created on 16 Sep 2020
@@ -27,12 +27,22 @@ class to read and parse incoming data from the receiver. It places
"""
import logging
-import socket
import ssl
from datetime import datetime, timedelta
+from io import BufferedReader
from queue import Empty
+from socket import (
+ AF_INET,
+ AF_INET6,
+ SOCK_DGRAM,
+ SOCK_STREAM,
+ gaierror,
+ getaddrinfo,
+ socket,
+)
from threading import Event, Thread
from time import sleep
+from tkinter import Frame, Label, Tk
from certifi import where as findcacerts
from pygnssutils import (
@@ -88,11 +98,11 @@ def __init__(self, app):
self._stopevent = Event()
self._ttyevent = Event()
- def start(self, caller: object, settings: dict):
+ def start(self, caller: Frame, settings: dict):
"""
Start the stream read thread.
- :param caller owner: calling object
+ :param Frame caller: calling Frame
:param dict settings: settings dictionary
"""
@@ -100,9 +110,10 @@ def start(self, caller: object, settings: dict):
self._stream_thread = Thread(
target=self._read_thread,
args=(
- caller,
+ self.__master,
self._stopevent,
settings,
+ caller.status_label, # for status update messages
),
daemon=True,
)
@@ -118,9 +129,10 @@ def stop(self):
def _read_thread(
self,
- caller,
+ master: Tk,
stopevent: Event,
settings: dict,
+ status: Label,
):
"""
THREADED PROCESS
@@ -161,6 +173,7 @@ def _read_thread(
) as stream:
if settings["protocol"] & TTY_PROTOCOL:
self._readlooptty(
+ master,
stopevent,
stream,
settings,
@@ -168,6 +181,7 @@ def _read_thread(
)
else:
self._readloop(
+ master,
stopevent,
stream,
settings,
@@ -178,6 +192,7 @@ def _read_thread(
in_filepath = settings["in_filepath"]
with open(in_filepath, "rb") as stream:
self._readloop(
+ master,
stopevent,
stream,
settings,
@@ -191,16 +206,16 @@ def _read_thread(
https = int(soc.https.get())
selfsign = int(soc.selfsign.get())
if soc.protocol.get()[-4:] == "IPv6":
- afam = socket.AF_INET6
- conn = socket.getaddrinfo(server, port)[1][4]
+ afam = AF_INET6
+ conn = getaddrinfo(server, port)[1][4]
else: # IPv4
- afam = socket.AF_INET
+ afam = AF_INET
conn = (server, port)
if soc.protocol.get()[:3] == "UDP":
- socktype = socket.SOCK_DGRAM
+ socktype = SOCK_DGRAM
else: # TCP
- socktype = socket.SOCK_STREAM
- with socket.socket(afam, socktype) as stream:
+ socktype = SOCK_STREAM
+ with socket(afam, socktype) as stream:
if https:
context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
context.load_verify_locations(findcacerts())
@@ -211,9 +226,10 @@ def _read_thread(
context.check_hostname = False
stream = context.wrap_socket(stream, server_hostname=server)
stream.connect(conn)
- if socktype == socket.SOCK_DGRAM:
+ if socktype == SOCK_DGRAM:
stream.send(b"") # send empty datagram to establish connection
self._readloop(
+ master,
stopevent,
stream,
settings,
@@ -224,6 +240,7 @@ def _read_thread(
with UBXSimulator() as stream:
if settings["protocol"] & TTY_PROTOCOL:
self._readlooptty(
+ master,
stopevent,
stream,
settings,
@@ -231,6 +248,7 @@ def _read_thread(
)
else:
self._readloop(
+ master,
stopevent,
stream,
settings,
@@ -239,28 +257,29 @@ def _read_thread(
except EOFError:
stopevent.set()
- self.__master.event_generate(settings["eof_event"])
+ master.event_generate(settings["eof_event"])
except TimeoutError:
stopevent.set()
- self.__master.event_generate(settings["timeout_event"])
+ master.event_generate(settings["timeout_event"])
except (
IOError,
SerialException,
SerialTimeoutException,
OSError,
AttributeError,
- socket.gaierror,
+ gaierror,
) as err:
if not stopevent.is_set():
stopevent.set()
- self.__master.event_generate(settings["error_event"])
- if hasattr(caller, "set_status"):
- caller.set_status(str(err), ERRCOL)
+ master.event_generate(settings["error_event"])
+ # use after(0) to avoid tkinter main thread contention
+ status.after(0, status.config, {"text": str(err), "fg": ERRCOL})
def _readloop(
self,
+ master: Tk,
stopevent: Event,
- stream: object,
+ stream: Serial | BufferedReader | socket,
settings: dict,
inactivity: int,
):
@@ -272,7 +291,7 @@ def _readloop(
prevent thrashing.
:param Event stopevent: thread stop event
- :param object stream: serial data stream
+ :param Serial | BufferedReader stream: serial data stream
:param dict settings: settings dictionary
:param int inactivity: inactivity timeout (s)
"""
@@ -286,7 +305,7 @@ def _errorhandler(err: Exception):
parsed_data = f"Error parsing data stream {err}"
settings["inqueue"].put((raw_data, parsed_data))
- self.__master.event_generate(settings["read_event"])
+ master.event_generate(settings["read_event"])
conntype = settings["conntype"]
@@ -320,7 +339,7 @@ def _errorhandler(err: Exception):
raw_data, parsed_data = ubr.read()
if raw_data is not None:
settings["inqueue"].put((raw_data, parsed_data))
- self.__master.event_generate(settings["read_event"])
+ master.event_generate(settings["read_event"])
lastevent = datetime.now()
else: # timeout or eof
if conntype == CONNECTED_FILE:
@@ -331,7 +350,6 @@ def _errorhandler(err: Exception):
raise TimeoutError
if conntype == CONNECTED_FILE:
lastread = datetime.now()
- self.__master.update_idletasks()
# write any queued output data to serial stream
if conntype in (CONNECTED, CONNECTED_SOCKET):
@@ -365,10 +383,14 @@ def _errorhandler(err: Exception):
_errorhandler(err)
continue
+ # allow for any tkinter events e.g. dialogs
+ self.__app.update_idletasks()
+
def _readlooptty(
self,
+ master: Tk,
stopevent: Event,
- stream: object,
+ stream: Serial,
settings: dict,
delay: float = 0.0,
):
@@ -377,7 +399,7 @@ def _readlooptty(
TTY (ASCII) Read stream continously until stop event or stream error.
:param Event stopevent: thread stop event
- :param object stream: serial data stream
+ :param Serial stream: serial data stream
:param dict settings: settings dictionary
:param float delay: delay between commands (secs)
"""
@@ -391,7 +413,7 @@ def _errorhandler(err: Exception):
parsed_data = f"Error parsing data stream {err}"
settings["inqueue"].put((raw_data, parsed_data))
- self.__master.event_generate(settings["read_event"])
+ master.event_generate(settings["read_event"])
raw_data = None
while not stopevent.is_set():
@@ -421,7 +443,7 @@ def _errorhandler(err: Exception):
settings["inqueue"].put(
(raw_data, raw_data.decode(ASCII, errors=BSR))
)
- self.__master.event_generate(settings["read_event"])
+ master.event_generate(settings["read_event"])
except (ValueError, SerialException) as err:
_errorhandler(err)
diff --git a/src/pygpsclient/strings.py b/src/pygpsclient/strings.py
index 43eab2bb..31791e2d 100644
--- a/src/pygpsclient/strings.py
+++ b/src/pygpsclient/strings.py
@@ -44,10 +44,11 @@
FILEOPENERROR = "Error opening file {}"
INACTIVE_TIMEOUT = "Inactivity timeout"
KILLSWITCH = "Running threads terminated by user"
-LOADCONFIGNK = "Unrecognised configuration setting '{}: {}'; using defaults"
-LOADCONFIGBAD = "Configuration not loaded {} {}; using defaults"
-LOADCONFIGOK = "Configuration loaded {}"
-LOADCONFIGNONE = "Configuration file not found {}; using defaults"
+LOADCONFIGNK = "Unrecognised configuration setting '{}: {}'"
+LOADCONFIGBAD = "Configuration not loaded {} {}. Using defaults"
+LOADCONFIGOK = "Configuration loaded {}{}"
+LOADCONFIGRESAVE = ". Consider re-saving"
+LOADCONFIGNONE = "Configuration file not found {}. Using defaults"
MAPCONFIGERR = "Custom map configuration error"
MAPOPENERR = "Unable to open custom map:\n{}"
MQTTCONN = "Connecting to MQTT server {}..."
@@ -137,7 +138,7 @@
LBLSERVERMODE = "Mode"
LBLSERVERPORT = "Port"
LBLSET = "Settings"
-LBLSHOWUNUSED = "Show Unused Satellites"
+LBLSHOWUNUSED = "Include C/N0 = 0"
LBLSOCKSERVE = "Socket Server /\nNTRIP Caster " # padded to align
LBLSPARTNCONFIG = "SPARTN Client"
LBLSPARTNGN = "GNSS RECEIVER CONFIGURATION (F9*)"
@@ -189,29 +190,3 @@
DLGTIMPORTMAP = "Import Custom Map"
DLGTTTY = "TTY Commands"
DLGNOTLS = "TLS certificate '{hostpem}' not found"
-
-# UBX Preset Command Descriptions
-PSTALLINFOFF = "CFG-INF - Turn OFF all non-error INF msgs"
-PSTALLINFON = "CFG-INF - Turn ON all INF msgs"
-PSTALLLOGOFF = "CFG-MSG - Turn OFF all LOG msgs"
-PSTALLLOGON = "CFG-MSG - Turn ON all LOG msgs"
-PSTALLMONOFF = "CFG-MSG - Turn OFF all MON msgs"
-PSTALLMONON = "CFG-MSG - Turn ON all MON msgs"
-PSTALLNMEAOFF = "CFG-MSG - Turn OFF all NMEA msgs"
-PSTALLNMEAON = "CFG-MSG - Turn ON all NMEA msgs"
-PSTALLRXMOFF = "CFG-MSG - Turn OFF all RXM msgs"
-PSTALLRXMON = "CFG-MSG - Turn ON all RXM msgs"
-PSTALLSECOFF = "CFG-MSG - Turn OFF all SEC msgs"
-PSTALLSECON = "CFG-MSG - Turn ON all SEC msgs"
-PSTALLUBXOFF = "CFG-MSG - Turn OFF all UBX NAV msgs"
-PSTALLUBXON = "CFG-MSG - Turn ON all UBX NAV msgs"
-PSTMINNMEAON = "CFG-MSG - Turn ON minimum NMEA msgs"
-PSTMINUBXON = "CFG-MSG - Turn ON minimum UBX NAV msgs"
-PSTPOLLALLCFG = "CFG-xxx - Poll All Configuration Messages"
-PSTPOLLALLNAV = "NAV(2)-xxx - Poll All Navigation Messages"
-PSTPOLLINFO = "CFG-INF - Poll Info message config"
-PSTPOLLPORT = "CFG-PRT - Poll Port config"
-PSTRESET = "CFG-CFG - RESTORE FACTORY DEFAULTS"
-PSTSAVE = "CFG-CFG - Save configuration to non-volatile memory"
-PSTUSENMEA = "CFG-MSG - Enable NMEA, Suppress UBX"
-PSTUSEUBX = "CFG-MSG - Enable UBX, Suppress NMEA"
diff --git a/src/pygpsclient/sysmon_frame.py b/src/pygpsclient/sysmon_frame.py
index 1853a3a0..6a725724 100644
--- a/src/pygpsclient/sysmon_frame.py
+++ b/src/pygpsclient/sysmon_frame.py
@@ -14,7 +14,7 @@
:license: BSD 3-Clause
"""
-from tkinter import ALL, NW, Canvas, E, Frame, IntVar, N, Radiobutton, S, W
+from tkinter import ALL, EW, NSEW, NW, Canvas, E, Frame, IntVar, Radiobutton, W
from pyubx2 import BOOTTYPE, UBXMessage
@@ -106,8 +106,8 @@ def _body(self):
bg=BGCOL,
# selectcolor=BGCOL,
)
- self._can_sysmon.grid(column=0, row=0, padx=0, pady=0, sticky=(N, S, W, E))
- self._frm_status.grid(column=0, row=1, padx=2, pady=2, sticky=(W, E))
+ self._can_sysmon.grid(column=0, row=0, padx=0, pady=0, sticky=NSEW)
+ self._frm_status.grid(column=0, row=1, padx=2, pady=2, sticky=EW)
self._rad_actual.grid(column=0, row=0, padx=0, pady=0, sticky=W)
self._rad_pending.grid(column=1, row=0, padx=0, pady=0, sticky=W)
self.grid_columnconfigure(0, weight=1)
diff --git a/src/pygpsclient/toplevel_dialog.py b/src/pygpsclient/toplevel_dialog.py
index 413410ed..f4f25285 100644
--- a/src/pygpsclient/toplevel_dialog.py
+++ b/src/pygpsclient/toplevel_dialog.py
@@ -12,9 +12,13 @@
:license: BSD 3-Clause
"""
+import logging
from tkinter import (
ALL,
+ EW,
HORIZONTAL,
+ NS,
+ NSEW,
NW,
VERTICAL,
Button,
@@ -22,10 +26,7 @@
E,
Frame,
Label,
- N,
- S,
Scrollbar,
- StringVar,
Toplevel,
W,
)
@@ -33,6 +34,7 @@
from PIL import Image, ImageTk
from pygpsclient.globals import (
+ APPNAME,
ERRCOL,
ICON_BLANK,
ICON_CONFIRMED,
@@ -46,11 +48,13 @@
ICON_SEND,
ICON_START,
ICON_WARNING,
+ INFOCOL,
MINHEIGHT,
MINWIDTH,
RESIZE,
)
from pygpsclient.helpers import check_lowres
+from pygpsclient.strings import DLG
class ToplevelDialog(Toplevel):
@@ -70,6 +74,7 @@ def __init__(self, app, dlgname: str, dim: tuple = (MINHEIGHT, MINWIDTH)):
self.__app = app # Reference to main application class
self.__master = self.__app.appmaster # Reference to root class (Tk)
self._dlgname = dlgname
+ self.logger = logging.getLogger(f"{APPNAME}.{dlgname}")
self.lowres, (self.height, self.width) = check_lowres(self.__master, dim)
super().__init__()
@@ -94,7 +99,6 @@ def __init__(self, app, dlgname: str, dim: tuple = (MINHEIGHT, MINWIDTH)):
self.img_send = ImageTk.PhotoImage(Image.open(ICON_SEND))
self.img_start = ImageTk.PhotoImage(Image.open(ICON_START))
self.img_warn = ImageTk.PhotoImage(Image.open(ICON_WARNING))
- self._status = StringVar()
self._con_body()
@@ -124,9 +128,9 @@ def _con_body(self):
self._frm_container = Frame(
self._can_container, borderwidth=2, relief="groove"
)
- self._can_container.grid(column=0, row=0, sticky=(N, S, E, W))
- x_scrollbar.grid(column=0, row=1, sticky=(E, W))
- y_scrollbar.grid(column=1, row=0, sticky=(N, S))
+ self._can_container.grid(column=0, row=0, sticky=NSEW)
+ x_scrollbar.grid(column=0, row=1, sticky=EW)
+ y_scrollbar.grid(column=1, row=0, sticky=NS)
x_scrollbar.config(command=self._can_container.xview)
y_scrollbar.config(command=self._can_container.yview)
# ensure container canvas expands to accommodate child frames
@@ -141,11 +145,11 @@ def _con_body(self):
)
else: # normal resolution
self._frm_container = Frame(self, borderwidth=2, relief="groove")
- self._frm_container.grid(column=0, row=0, sticky=(N, S, E, W))
+ self._frm_container.grid(column=0, row=0, sticky=NSEW)
# create status frame
self._frm_status = Frame(self, borderwidth=2, relief="groove")
- self._lbl_status = Label(self._frm_status, textvariable=self._status, anchor=W)
+ self._lbl_status = Label(self._frm_status, anchor=W)
self._btn_exit = Button(
self._frm_status,
image=self.img_exit,
@@ -153,8 +157,8 @@ def _con_body(self):
fg=ERRCOL,
command=self.on_exit,
)
- self._frm_status.grid(column=0, row=2, sticky=(W, E))
- self._lbl_status.grid(column=0, row=0, sticky=(W, E))
+ self._frm_status.grid(column=0, row=2, sticky=EW)
+ self._lbl_status.grid(column=0, row=0, sticky=EW)
self._btn_exit.grid(column=1, row=0, sticky=E)
# set column and row weights
@@ -176,28 +180,14 @@ def _finalise(self):
Finalise Toplevel window after child frames have been created.
"""
- # self.set_status(f"{self.height}, {self.width}") # testing only
-
- def set_status(self, message: str, color: str = ""):
- """
- Set status message.
-
- :param str message: message to be displayed
- :param str color: rgb color of text (blue)
- """
-
- message = (message[:120] + "..") if len(message) > 120 else message
- if color != "":
- self._lbl_status.config(fg=color)
- self._status.set(" " + message)
- self.update_idletasks()
+ # self.status_label = (f"{self.height}, {self.width}") # testing only
def on_exit(self, *args, **kwargs): # pylint: disable=unused-argument
"""
Handle Exit button press.
"""
- self.__app.stop_dialog(self._dlgname)
+ self.__app.dialog_state.state[self._dlgname][DLG] = None
self.destroy()
def _on_resize(self, event): # pylint: disable=unused-argument
@@ -230,3 +220,38 @@ def container(self):
"""
return self._frm_container
+
+ @property
+ def status_label(self) -> Label:
+ """
+ Getter for status_label.
+
+ :param self: Description
+ :return: Description
+ :rtype: Label
+ """
+
+ return self._lbl_status
+
+ @status_label.setter
+ def status_label(self, message: str | tuple[str, str]):
+ """
+ Setter for status_label.
+
+ :param self: Description
+ :param tuple | str message: (message, color))
+ """
+
+ if isinstance(message, tuple):
+ message, color = message
+ else:
+ color = INFOCOL
+
+ # truncate very long messages
+ if len(message) > 100:
+ message = "..." + message[-100:]
+
+ self.status_label.after(
+ 0, self.status_label.config, {"text": message, "fg": color}
+ )
+ self.update_idletasks()
diff --git a/src/pygpsclient/tty_preset_dialog.py b/src/pygpsclient/tty_preset_dialog.py
index 07e102e0..f1bd7761 100644
--- a/src/pygpsclient/tty_preset_dialog.py
+++ b/src/pygpsclient/tty_preset_dialog.py
@@ -11,8 +11,12 @@
"""
from tkinter import (
+ EW,
HORIZONTAL,
LEFT,
+ NE,
+ NS,
+ NSEW,
VERTICAL,
Button,
Checkbutton,
@@ -146,17 +150,17 @@ def _do_layout(self):
"""
self._frm_body.grid(
- column=0, row=0, padx=5, pady=5, ipadx=5, ipady=5, sticky=(N, S, E, W)
+ column=0, row=0, padx=5, pady=5, ipadx=5, ipady=5, sticky=NSEW
)
self._lbl_command.grid(column=0, row=0, padx=3, sticky=W)
- self._ent_command.grid(column=1, row=0, columnspan=3, padx=3, sticky=(W, E))
+ self._ent_command.grid(column=1, row=0, columnspan=3, padx=3, sticky=EW)
self._chk_crlf.grid(column=0, row=1, padx=3, sticky=W)
self._chk_echo.grid(column=1, row=1, padx=3, sticky=W)
self._chk_delay.grid(column=2, row=1, padx=3, sticky=W)
ttk.Separator(self._frm_body).grid(
- column=0, row=2, columnspan=4, padx=2, pady=2, sticky=(W, E)
+ column=0, row=2, columnspan=4, padx=2, pady=2, sticky=EW
)
- self._lbl_presets.grid(column=0, row=3, columnspan=3, padx=3, sticky=(W, E))
+ self._lbl_presets.grid(column=0, row=3, columnspan=3, padx=3, sticky=EW)
self._lbx_preset.grid(
column=0,
row=3,
@@ -164,15 +168,15 @@ def _do_layout(self):
rowspan=10,
padx=3,
pady=3,
- sticky=(N, S, W, E),
+ sticky=NS,
)
self._scr_presetv.grid(column=2, row=3, rowspan=21, sticky=(N, S, E))
- self._scr_preseth.grid(column=0, row=24, columnspan=3, sticky=(W, E))
+ self._scr_preseth.grid(column=0, row=24, columnspan=3, sticky=EW)
self._btn_send_command.grid(
- column=3, row=3, padx=3, ipadx=3, ipady=3, sticky=(N, E)
+ column=3, row=3, padx=3, ipadx=3, ipady=3, sticky=NE
)
self._lbl_send_command.grid(
- column=3, row=4, padx=3, ipadx=3, ipady=3, sticky=(N, W, E)
+ column=3, row=4, padx=3, ipadx=3, ipady=3, sticky=EW
)
self.container.grid_columnconfigure(0, weight=10)
self.container.grid_rowconfigure(0, weight=10)
@@ -228,13 +232,13 @@ def _on_select_preset(self, *args, **kwargs): # pylint: disable=unused-argument
"""
try:
- self.set_status("", INFOCOL)
+ self.status_label = ("", INFOCOL)
idx = self._lbx_preset.curselection()
preset = self._lbx_preset.get(idx).split(";", 1)
self._confirm = CONFIRM in preset[0]
self._command.set(preset[1])
except IndexError:
- self.set_status("Invalid preset format", ERRCOL)
+ self.status_label = ("Invalid preset format", ERRCOL)
def _on_send_command(self, *args, **kwargs): # pylint: disable=unused-argument
"""
@@ -242,7 +246,7 @@ def _on_send_command(self, *args, **kwargs): # pylint: disable=unused-argument
"""
if self._command.get() in ("", None):
- self.set_status("Enter or select command", ERRCOL)
+ self.status_label = ("Enter or select command", ERRCOL)
return
try:
@@ -257,15 +261,15 @@ def _on_send_command(self, *args, **kwargs): # pylint: disable=unused-argument
status = CONFIRMED
if status == CONFIRMED:
self._lbl_send_command.config(image=self.img_pending)
- self.set_status("Command(s) sent")
+ self.status_label = "Command(s) sent"
elif status == CANCELLED:
- self.set_status("Command(s) cancelled")
+ self.status_label = "Command(s) cancelled"
elif status == NOMINAL:
- self.set_status("Command(s) sent, no results")
+ self.status_label = "Command(s) sent, no results"
self._confirm = False
except Exception as err: # pylint: disable=broad-except
- self.set_status(f"Error {err}", ERRCOL)
+ self.status_label = (f"Error {err}", ERRCOL)
self._lbl_send_command.config(image=self.img_warn)
def _parse_command(self, command: str):
@@ -290,7 +294,7 @@ def _parse_command(self, command: str):
(cmd, cmd.decode(ASCII, errors=BSR), TTYMARKER)
)
except Exception as err: # pylint: disable=broad-except
- self.set_status(f"Error {err}", ERRCOL)
+ self.status_label = (f"Error {err}", ERRCOL)
self._lbl_send_command.config(image=self.img_warn)
def update_status(self, msg: bytes):
@@ -304,10 +308,10 @@ def update_status(self, msg: bytes):
for ack in TTYOK:
if ack in msgstr:
self._lbl_send_command.config(image=self.img_confirmed)
- self.set_status("Command(s) acknowledged", OKCOL)
+ self.status_label = ("Command(s) acknowledged", OKCOL)
return
for nak in TTYERR:
if nak in msgstr:
self._lbl_send_command.config(image=self.img_warn)
- self.set_status("Command(s) rejected", ERRCOL)
+ self.status_label = ("Command(s) rejected", ERRCOL)
break
diff --git a/src/pygpsclient/ubx_cfgval_frame.py b/src/pygpsclient/ubx_cfgval_frame.py
index a8b5bd2f..7a4059ba 100644
--- a/src/pygpsclient/ubx_cfgval_frame.py
+++ b/src/pygpsclient/ubx_cfgval_frame.py
@@ -13,6 +13,7 @@
# pylint: disable=no-member
from tkinter import (
+ EW,
HORIZONTAL,
LEFT,
NORMAL,
@@ -200,17 +201,17 @@ def _do_layout(self):
Layout widgets.
"""
- self._lbl_configdb.grid(column=0, row=0, columnspan=6, padx=3, sticky=(W, E))
- self._lbl_cat.grid(column=0, row=1, padx=3, sticky=(W, E))
- self._lbx_cat.grid(column=0, row=2, rowspan=5, padx=3, pady=3, sticky=(W, E))
+ self._lbl_configdb.grid(column=0, row=0, columnspan=6, padx=3, sticky=EW)
+ self._lbl_cat.grid(column=0, row=1, padx=3, sticky=EW)
+ self._lbx_cat.grid(column=0, row=2, rowspan=5, padx=3, pady=3, sticky=EW)
self._scr_catv.grid(column=0, row=2, rowspan=5, sticky=(N, S, E))
- self._scr_cath.grid(column=0, row=7, sticky=(W, E))
- self._lbl_parm.grid(column=1, row=1, columnspan=4, padx=3, sticky=(W, E))
+ self._scr_cath.grid(column=0, row=7, sticky=EW)
+ self._lbl_parm.grid(column=1, row=1, columnspan=4, padx=3, sticky=EW)
self._lbx_parm.grid(
- column=1, row=2, columnspan=4, rowspan=5, padx=3, pady=3, sticky=(W, E)
+ column=1, row=2, columnspan=4, rowspan=5, padx=3, pady=3, sticky=EW
)
self._scr_parmv.grid(column=4, row=2, rowspan=5, sticky=(N, S, E))
- self._scr_parmh.grid(column=1, row=7, columnspan=4, sticky=(W, E))
+ self._scr_parmh.grid(column=1, row=7, columnspan=4, sticky=EW)
self._rad_cfgget.grid(column=0, row=8, padx=3, pady=0, sticky=W)
self._rad_cfgset.grid(column=0, row=9, padx=3, pady=0, sticky=W)
self._rad_cfgdel.grid(column=0, row=10, padx=3, pady=0, sticky=W)
@@ -221,9 +222,7 @@ def _do_layout(self):
self._lbl_layer.grid(column=1, row=9, padx=3, pady=0, sticky=E)
self._spn_layer.grid(column=2, row=9, padx=3, pady=0, sticky=W)
self._lbl_val.grid(column=1, row=10, padx=3, pady=0, sticky=E)
- self._ent_val.grid(
- column=2, row=10, columnspan=3, padx=3, pady=0, sticky=(W, E)
- )
+ self._ent_val.grid(column=2, row=10, columnspan=3, padx=3, pady=0, sticky=EW)
self._btn_send_command.grid(
column=3, row=12, rowspan=2, ipadx=3, ipady=3, sticky=E
@@ -334,7 +333,7 @@ def _do_valset(self):
atts = attsiz(self._cfgatt.get())
except ValueError as err:
self._ent_val.validate(VALNONBLANK)
- self.__container.set_status(f"INVALID ENTRY - {err}", ERRCOL)
+ self.__container.status_label = (f"INVALID ENTRY - {err}", ERRCOL)
return False
val = self._cfgval.get()
layers = self._cfglayer.get()
@@ -380,15 +379,13 @@ def _do_valset(self):
msg = UBXMessage.config_set(layers, transaction, cfgData)
self.__container.send_command(msg)
self._lbl_send_command.config(image=self._img_pending)
- self.__container.set_status(
- "CFG-VALSET SET message sent",
- )
+ self.__container.status_label = "CFG-VALSET SET message sent"
for msgid in ("ACK-ACK", "ACK-NAK"):
self.__container.set_pending(msgid, UBX_CFGVAL)
else:
self._lbl_send_command.config(image=self._img_warn)
typ = ATTDICT[att]
- self.__container.set_status(
+ self.__container.status_label = (
(
"INVALID ENTRY - must conform to parameter "
f"type {att} ({typ}) and size {atts} bytes"
@@ -417,7 +414,7 @@ def _do_valdel(self):
msg = UBXMessage.config_del(layers, transaction, key)
self.__container.send_command(msg)
self._lbl_send_command.config(image=self._img_pending)
- self.__container.set_status("CFG-VALDEL SET message sent")
+ self.__container.status_label = "CFG-VALDEL SET message sent"
for msgid in ("ACK-ACK", "ACK-NAK"):
self.__container.set_pending(msgid, UBX_CFGVAL)
@@ -442,7 +439,7 @@ def _do_valget(self):
msg = UBXMessage.config_poll(layers, transaction, keys)
self.__container.send_command(msg)
self._lbl_send_command.config(image=self._img_pending)
- self.__container.set_status("CFG-VALGET POLL message sent")
+ self.__container.status_label = "CFG-VALGET POLL message sent"
for msgid in ("CFG-VALGET", "ACK-ACK", "ACK-NAK"):
self.__container.set_pending(msgid, UBX_CFGVAL)
@@ -460,12 +457,12 @@ def update_status(self, msg: UBXMessage): # pylint: disable=unused-argument
if isinstance(val, bytes):
val = val.hex()
self._cfgval.set(val)
- self.__container.set_status("CFG-VALGET GET message received", OKCOL)
+ self.__container.status_label = ("CFG-VALGET GET message received", OKCOL)
elif msg.identity == "ACK-ACK":
self._lbl_send_command.config(image=self._img_confirmed)
- self.__container.set_status("CFG-VAL command acknowledged", OKCOL)
+ self.__container.status_label = ("CFG-VAL command acknowledged", OKCOL)
elif msg.identity == "ACK-NAK":
self._lbl_send_command.config(image=self._img_warn)
- self.__container.set_status("CFG-VAL command rejected", ERRCOL)
+ self.__container.status_label = ("CFG-VAL command rejected", ERRCOL)
diff --git a/src/pygpsclient/ubx_config_dialog.py b/src/pygpsclient/ubx_config_dialog.py
index ac7ae4f6..bb3a4c4c 100644
--- a/src/pygpsclient/ubx_config_dialog.py
+++ b/src/pygpsclient/ubx_config_dialog.py
@@ -23,7 +23,7 @@
:license: BSD 3-Clause
"""
-from tkinter import E, N, S, W
+from tkinter import NSEW
from pyubx2 import UBXMessage
@@ -127,6 +127,7 @@ def _do_layout(self):
# top of grid
col = 0
row = 0
+ colsp = 0
for frm in (
self._frm_device_info,
self._frm_recorder,
@@ -140,7 +141,7 @@ def _do_layout(self):
row=row,
columnspan=colsp,
rowspan=rowsp,
- sticky=(N, S, W, E),
+ sticky=NSEW,
)
row += rowsp
# middle column of grid
@@ -153,7 +154,7 @@ def _do_layout(self):
row=row,
columnspan=colsp,
rowspan=rowsp,
- sticky=(N, S, W, E),
+ sticky=NSEW,
)
row += rowsp
# right column of grid
@@ -167,7 +168,7 @@ def _do_layout(self):
row=row,
columnspan=colsp,
rowspan=rowsp,
- sticky=(N, S, W, E),
+ sticky=NSEW,
)
row += rowsp
@@ -185,7 +186,7 @@ def _reset(self):
CONNECTED_SOCKET,
CONNECTED_SIMULATOR,
):
- self.set_status("Device not connected", ERRCOL)
+ self.status_label = ("Device not connected", ERRCOL)
def _attach_events(self):
"""
diff --git a/src/pygpsclient/ubx_handler.py b/src/pygpsclient/ubx_handler.py
index 3e3138a6..560e5f0f 100644
--- a/src/pygpsclient/ubx_handler.py
+++ b/src/pygpsclient/ubx_handler.py
@@ -16,6 +16,7 @@
"""
import logging
+from time import time
from pyubx2 import UBXMessage, itow2utc
@@ -323,9 +324,9 @@ def _process_NAV_SAT(self, data: UBXMessage):
:param UBXMessage data: NAV-SAT parsed message
"""
- show_unused = self.__app.configuration.get("unusedsat_b")
self.gsv_data = {}
num_siv = int(data.numSvs)
+ now = time()
for i in range(num_siv):
idx = f"_{i+1:02d}"
@@ -337,9 +338,7 @@ def _process_NAV_SAT(self, data: UBXMessage):
elev = getattr(data, "elev" + idx)
azim = getattr(data, "azim" + idx)
cno = getattr(data, "cno" + idx)
- if cno == 0 and not show_unused: # omit unused sats
- continue
- self.gsv_data[f"{gnssId}-{svid}"] = (gnssId, svid, elev, azim, cno)
+ self.gsv_data[(gnssId, svid)] = (gnssId, svid, elev, azim, cno, now)
self.__app.gnss_status.siv = len(self.gsv_data)
self.__app.gnss_status.gsv_data = self.gsv_data
@@ -352,7 +351,6 @@ def _process_NAV_STATUS(self, data: UBXMessage):
"""
self.__app.gnss_status.diff_corr = data.diffSoln
- # self.__app.gnss_status.diff_age = "<60"
self.__app.gnss_status.fix = fix2desc("NAV-STATUS", data.gpsFix)
if data.carrSoln > 0:
self.__app.gnss_status.fix = fix2desc("NAV-STATUS", data.carrSoln + 5)
diff --git a/src/pygpsclient/ubx_msgrate_frame.py b/src/pygpsclient/ubx_msgrate_frame.py
index f4cd2dcf..da70e3da 100644
--- a/src/pygpsclient/ubx_msgrate_frame.py
+++ b/src/pygpsclient/ubx_msgrate_frame.py
@@ -13,6 +13,7 @@
"""
from tkinter import (
+ EW,
LEFT,
VERTICAL,
Button,
@@ -159,10 +160,9 @@ def _do_layout(self):
"""
Layout widgets.
"""
-
- self._lbl_cfg_msg.grid(column=0, row=0, columnspan=6, padx=3, sticky=(W, E))
+ self._lbl_cfg_msg.grid(column=0, row=0, columnspan=6, padx=3, sticky=EW)
self._lbx_cfg_msg.grid(
- column=0, row=1, columnspan=2, rowspan=11, padx=3, pady=3, sticky=(W, E)
+ column=0, row=1, columnspan=2, rowspan=11, padx=3, pady=3, sticky=EW
)
self._scr_cfg_msg.grid(column=1, row=1, rowspan=11, sticky=(N, S, E))
self._lbl_usb.grid(column=2, row=1, rowspan=2, padx=0, pady=1, sticky=E)
@@ -215,7 +215,7 @@ def update_status(self, msg: UBXMessage):
"""
if msg.identity == "CFG-MSG":
- self.__container.set_status("CFG-MSG GET message received", OKCOL)
+ self.__container.status_label = ("CFG-MSG GET message received", OKCOL)
self._ddc_rate.set(msg.rateDDC)
self._uart1_rate.set(msg.rateUART1)
self._uart2_rate.set(msg.rateUART2)
@@ -224,7 +224,7 @@ def update_status(self, msg: UBXMessage):
self._lbl_send_command.config(image=self._img_confirmed)
elif msg.identity == "ACK-NAK":
- self.__container.set_status("CFG-MSG POLL message rejected", ERRCOL)
+ self.__container.status_label = ("CFG-MSG POLL message rejected", ERRCOL)
self._lbl_send_command.config(image=self._img_warn)
def _on_select_cfg_msg(self, *args, **kwargs): # pylint: disable=unused-argument
@@ -266,7 +266,7 @@ def _on_send_cfg_msg(self, *args, **kwargs): # pylint: disable=unused-argument
)
self.__container.send_command(msg)
self._lbl_send_command.config(image=self._img_pending)
- self.__container.set_status("CFG-MSG SET message sent", OKCOL)
+ self.__container.status_label = "CFG-MSG SET message sent"
for msgid in ("ACK-ACK", "ACK-NAK"):
self.__container.set_pending(msgid, UBX_CFGMSG)
@@ -282,8 +282,6 @@ def _do_poll_msg(self, msgtyp: bytes):
msg = UBXMessage("CFG", "CFG-MSG", POLL, payload=msgtyp)
self.__container.send_command(msg)
self._lbl_send_command.config(image=self._img_pending)
- self.__container.set_status(
- "CFG-MSG POLL message sent",
- )
+ self.__container.status_label = "CFG-MSG POLL message sent"
for msgid in ("CFG-MSG", "ACK-NAK"):
self.__container.set_pending(msgid, UBX_CFGMSG)
diff --git a/src/pygpsclient/ubx_port_frame.py b/src/pygpsclient/ubx_port_frame.py
index 636f719b..314c0037 100644
--- a/src/pygpsclient/ubx_port_frame.py
+++ b/src/pygpsclient/ubx_port_frame.py
@@ -10,7 +10,18 @@
:license: BSD 3-Clause
"""
-from tkinter import Button, Checkbutton, E, Frame, IntVar, Label, Spinbox, StringVar, W
+from tkinter import (
+ EW,
+ Button,
+ Checkbutton,
+ E,
+ Frame,
+ IntVar,
+ Label,
+ Spinbox,
+ StringVar,
+ W,
+)
from PIL import Image, ImageTk
from pyubx2 import POLL, SET, UBXMessage
@@ -132,7 +143,7 @@ def _do_layout(self):
Layout widgets.
"""
- self._lbl_cfg_port.grid(column=0, row=0, columnspan=6, padx=3, sticky=(W, E))
+ self._lbl_cfg_port.grid(column=0, row=0, columnspan=6, padx=3, sticky=EW)
self._lbl_ubx_portid.grid(
column=0, row=1, columnspan=1, rowspan=2, padx=3, sticky=W
)
@@ -197,10 +208,10 @@ def update_status(self, msg: UBXMessage):
self._outprot_nmea.set(msg.outNMEA)
self._outprot_rtcm3.set(msg.outRTCM3)
self._lbl_send_command.config(image=self._img_confirmed)
- self.__container.set_status("CFG-PRT GET message received", OKCOL)
+ self.__container.status_label = ("CFG-PRT GET message received", OKCOL)
elif msg.identity == "ACK-NAK":
- self.__container.set_status("CFG-PRT POLL message rejected", ERRCOL)
+ self.__container.status_label = ("CFG-PRT POLL message rejected", ERRCOL)
self._lbl_send_command.config(image=self._img_warn)
def _on_select_portid(self):
@@ -243,9 +254,7 @@ def _on_send_port(self, *args, **kwargs): # pylint: disable=unused-argument
)
self.__container.send_command(msg)
self._lbl_send_command.config(image=self._img_pending)
- self.__container.set_status(
- "CFG-PRT SET message sent",
- )
+ self.__container.status_label = "CFG-PRT SET message sent"
for msgid in ("ACK-NAK", "ACK-NAK"):
self.__container.set_pending(msgid, UBX_CFGPRT)
@@ -260,8 +269,6 @@ def _do_poll_prt(self, *args, **kwargs): # pylint: disable=unused-argument
msg = UBXMessage("CFG", "CFG-PRT", POLL, portID=portID)
self.__container.send_command(msg)
self._lbl_send_command.config(image=self._img_pending)
- self.__container.set_status(
- "CFG-PRT POLL message sent",
- )
+ self.__container.status_label = "CFG-PRT POLL message sent"
for msgid in ("CFG-PRT", "ACK-NAK"):
self.__container.set_pending(msgid, UBX_CFGPRT)
diff --git a/src/pygpsclient/ubx_preset_frame.py b/src/pygpsclient/ubx_preset_frame.py
index d3db7fe3..673cf710 100644
--- a/src/pygpsclient/ubx_preset_frame.py
+++ b/src/pygpsclient/ubx_preset_frame.py
@@ -12,6 +12,7 @@
import logging
from tkinter import (
+ EW,
HORIZONTAL,
LEFT,
VERTICAL,
@@ -118,14 +119,14 @@ def _do_layout(self):
Layout widgets.
"""
- self._lbl_presets.grid(column=0, row=0, columnspan=6, padx=3, sticky=(W, E))
+ self._lbl_presets.grid(column=0, row=0, columnspan=6, padx=3, sticky=EW)
self._lbx_preset.grid(
- column=0, row=1, columnspan=3, rowspan=12, padx=3, pady=3, sticky=(W, E)
+ column=0, row=1, columnspan=3, rowspan=12, padx=3, pady=3, sticky=EW
)
self._scr_presetv.grid(column=2, row=1, rowspan=12, sticky=(N, S, E))
- self._scr_preseth.grid(column=0, row=13, columnspan=3, sticky=(W, E))
- self._btn_send_command.grid(column=3, row=1, ipadx=3, ipady=3, sticky=(W, E))
- self._lbl_send_command.grid(column=3, row=3, ipadx=3, ipady=3, sticky=(W, E))
+ self._scr_preseth.grid(column=0, row=13, columnspan=3, sticky=EW)
+ self._btn_send_command.grid(column=3, row=1, ipadx=3, ipady=3, sticky=EW)
+ self._lbl_send_command.grid(column=3, row=3, ipadx=3, ipady=3, sticky=EW)
(cols, rows) = self.grid_size()
for i in range(cols):
@@ -164,7 +165,7 @@ def _on_send_preset(self, *args, **kwargs): # pylint: disable=unused-argument
"""
if self._preset_command in ("", None):
- self.__container.set_status("Select preset", ERRCOL)
+ self.__container.status_label = ("Select preset", ERRCOL)
return
status = CONFIRMED
@@ -183,22 +184,16 @@ def _on_send_preset(self, *args, **kwargs): # pylint: disable=unused-argument
if status == CONFIRMED:
self._lbl_send_command.config(image=self._img_pending)
- self.__container.set_status(
- "Command(s) sent",
- )
+ self.__container.status_label = "Command(s) sent"
for msgid in confids:
self.__container.set_pending(msgid, UBX_PRESET)
elif status == CANCELLED:
- self.__container.set_status(
- "Command(s) cancelled",
- )
+ self.__container.status_label = "Command(s) cancelled"
elif status == NOMINAL:
- self.__container.set_status(
- "Command(s) sent, no results",
- )
+ self.__container.status_label = "Command(s) sent, no results"
except Exception as err: # pylint: disable=broad-except
- self.__container.set_status(f"Error {err}", ERRCOL)
+ self.__container.status_label = (f"Error {err}", ERRCOL)
self._lbl_send_command.config(image=self._img_warn)
def _format_preset(self, command: str):
@@ -226,7 +221,7 @@ def _format_preset(self, command: str):
msg = UBXMessage(ubx_class, ubx_id, mode)
self.__container.send_command(msg)
except Exception as err: # pylint: disable=broad-except
- self.__app.set_status(f"Error {err}", ERRCOL)
+ self.__container.status_label = (f"Error {err}", ERRCOL)
self._lbl_send_command.config(image=self._img_warn)
def update_status(self, msg: UBXMessage):
@@ -238,7 +233,7 @@ def update_status(self, msg: UBXMessage):
if msg.identity in ("ACK-ACK", "MON-VER"):
self._lbl_send_command.config(image=self._img_confirmed)
- self.__container.set_status("Preset command(s) acknowledged", OKCOL)
+ self.__container.status_label = ("Preset command(s) acknowledged", OKCOL)
elif msg.identity == "ACK-NAK":
self._lbl_send_command.config(image=self._img_warn)
- self.__container.set_status("Preset command(s) rejected", ERRCOL)
+ self.__container.status_label = ("Preset command(s) rejected", ERRCOL)
diff --git a/src/pygpsclient/ubx_recorder_frame.py b/src/pygpsclient/ubx_recorder_frame.py
index 7f1cdd62..0fe56c95 100644
--- a/src/pygpsclient/ubx_recorder_frame.py
+++ b/src/pygpsclient/ubx_recorder_frame.py
@@ -18,7 +18,7 @@
from threading import Event, Thread
from time import sleep
-from tkinter import Button, E, Frame, Label, TclError, W, filedialog
+from tkinter import EW, Button, Frame, Label, TclError, W, filedialog
from PIL import Image, ImageTk
from pyubx2 import (
@@ -159,15 +159,15 @@ def _do_layout(self):
Layout widgets.
"""
- self._lbl_recorder.grid(column=0, row=0, columnspan=6, padx=3, sticky=(W, E))
+ self._lbl_recorder.grid(column=0, row=0, columnspan=6, padx=3, sticky=EW)
self._btn_load.grid(column=0, row=1, ipadx=3, ipady=3, sticky=W)
self._btn_save.grid(column=1, row=1, ipadx=3, ipady=3, sticky=W)
self._btn_play.grid(column=2, row=1, ipadx=3, ipady=3, sticky=W)
self._btn_record.grid(column=3, row=1, ipadx=3, ipady=3, sticky=W)
self._btn_undo.grid(column=4, row=1, ipadx=3, ipady=3, sticky=W)
self._btn_delete.grid(column=5, row=1, ipadx=3, ipady=3, sticky=W)
- self._lbl_status.grid(column=0, row=2, columnspan=6, padx=3, sticky=(W, E))
- self._lbl_activity.grid(column=0, row=3, columnspan=6, padx=3, sticky=(W, E))
+ self._lbl_status.grid(column=0, row=2, columnspan=6, padx=3, sticky=EW)
+ self._lbl_activity.grid(column=0, row=3, columnspan=6, padx=3, sticky=EW)
(cols, rows) = self.grid_size()
for i in range(cols):
@@ -206,6 +206,7 @@ def _open_configfile(self):
"""
return self.__app.file_handler.open_file(
+ self,
"ubx",
(
("ubx config files", "*.ubx"),
diff --git a/src/pygpsclient/ubx_solrate_frame.py b/src/pygpsclient/ubx_solrate_frame.py
index dd051025..070fb4a8 100644
--- a/src/pygpsclient/ubx_solrate_frame.py
+++ b/src/pygpsclient/ubx_solrate_frame.py
@@ -10,7 +10,7 @@
:license: BSD 3-Clause
"""
-from tkinter import Button, E, Frame, IntVar, Label, Spinbox, StringVar, W
+from tkinter import EW, Button, E, Frame, IntVar, Label, Spinbox, StringVar, W
from PIL import Image, ImageTk
from pyubx2 import POLL, SET, UBXMessage
@@ -118,7 +118,7 @@ def _do_layout(self):
Layout widgets.
"""
- self._lbl_cfg_rate.grid(column=0, row=0, columnspan=6, padx=3, sticky=(W, E))
+ self._lbl_cfg_rate.grid(column=0, row=0, columnspan=6, padx=3, sticky=EW)
self._lbl_ubx_measint.grid(
column=0, row=1, columnspan=2, rowspan=1, padx=3, pady=3, sticky=W
)
@@ -173,10 +173,10 @@ def update_status(self, msg: UBXMessage):
self._navrate.set(msg.navRate)
self._timeref.set(TIMEREFS[msg.timeRef])
self._lbl_send_command.config(image=self._img_confirmed)
- self.__container.set_status("CFG-RATE GET message received", OKCOL)
+ self.__container.status_label = ("CFG-RATE GET message received", OKCOL)
elif msg.identity == "ACK-NAK":
- self.__container.set_status("CFG-RATE POLL message rejected", ERRCOL)
+ self.__container.status_label = ("CFG-RATE POLL message rejected", ERRCOL)
self._lbl_send_command.config(image=self._img_warn)
def _on_send_rate(self, *args, **kwargs): # pylint: disable=unused-argument
@@ -200,9 +200,7 @@ def _on_send_rate(self, *args, **kwargs): # pylint: disable=unused-argument
)
self.__container.send_command(msg)
self._lbl_send_command.config(image=self._img_pending)
- self.__container.set_status(
- "CFG-RATE SET message sent",
- )
+ self.__container.status_label = "CFG-RATE SET message sent"
self.__container.set_pending(UBX_CFGRATE, ("ACK-ACK", "ACK-NAK"))
self._do_poll_rate()
@@ -215,8 +213,6 @@ def _do_poll_rate(self, *args, **kwargs): # pylint: disable=unused-argument
msg = UBXMessage("CFG", "CFG-RATE", POLL)
self.__container.send_command(msg)
self._lbl_send_command.config(image=self._img_pending)
- self.__container.set_status(
- "CFG-RATE POLL message sent",
- )
+ self.__container.status_label = "CFG-RATE POLL message sent"
for msgid in ("CFG-RATE", "ACK-NAK"):
self.__container.set_pending(msgid, UBX_CFGRATE)
diff --git a/src/pygpsclient/widget_state.py b/src/pygpsclient/widget_state.py
index 0917f993..ef30defd 100644
--- a/src/pygpsclient/widget_state.py
+++ b/src/pygpsclient/widget_state.py
@@ -22,7 +22,7 @@ class definition and update `ubx_handler` to populate them.
:license: BSD 3-Clause
"""
-from tkinter import E, N, S, W
+from tkinter import NSEW, E, S, W
from pygpsclient.banner_frame import BannerFrame
from pygpsclient.chart_frame import ChartviewFrame
@@ -85,7 +85,7 @@ def __init__(self):
CLASS: BannerFrame,
FRAME: "frm_banner",
VISIBLE: True,
- STICKY: (N, W, E, S),
+ STICKY: NSEW,
COL: 0,
ROW: 0,
COLSPAN: 6,
@@ -95,7 +95,7 @@ def __init__(self):
CLASS: SettingsFrame,
FRAME: "frm_settings",
VISIBLE: True,
- STICKY: (N, W, E, S),
+ STICKY: NSEW,
COL: 5,
ROW: 1,
ROWSPAN: 4,
diff --git a/tests/test_configs.py b/tests/test_configs.py
index 6cb9fe79..783e54f4 100644
--- a/tests/test_configs.py
+++ b/tests/test_configs.py
@@ -15,6 +15,7 @@
import unittest
from pygpsclient.receiver_config_handler import (
+ config_nmea,
config_disable_lc29h,
config_disable_lg290p,
config_disable_septentrio,
@@ -38,6 +39,11 @@ def setUp(self):
def tearDown(self):
pass
+ def test_config_nmea(self):
+ EXPECTED_RESULT = ""
+ res = config_nmea(1, "UART1")
+ self.assertEqual(str(res), EXPECTED_RESULT)
+
def test_config_disable_lc29h(self):
EXPECTED_RESULT = "[NMEAMessage('P','AIR062', 1, payload=['-1', '0']), NMEAMessage('P','AIR432', 1, payload=['-1']), NMEAMessage('P','AIR434', 1, payload=['0']), NMEAMessage('P','QTMSAVEPAR', 1, payload=[]), NMEAMessage('P','AIR005', 1, payload=[])]"
res = config_disable_lc29h()
diff --git a/tests/test_static.py b/tests/test_static.py
index ac88bf34..9b9da6bc 100644
--- a/tests/test_static.py
+++ b/tests/test_static.py
@@ -24,7 +24,6 @@
AreaXY,
Point,
TrackPoint,
- OKCOL,
UI,
UMM,
UIK,
@@ -73,11 +72,12 @@
svid2gnssid,
time2str,
ubx2preset,
+ unused_sats,
val2sphp,
wnotow2date,
xy2ll,
)
-from pygpsclient.mapquest import (
+from pygpsclient.mapquest_handler import (
compress_track,
format_mapquest_request,
mapq_compress,
@@ -107,8 +107,14 @@ def __init__(self):
self.appmaster = "appmaster"
self.widget_state = WidgetState()
self.file_handler = DummyFileHandler()
+ self.label_status = ""
- def set_status(self, message, color=OKCOL):
+ @property
+ def status_label(self) -> str:
+ return self.label_status
+
+ @status_label.setter
+ def status_label(self, message):
print(message)
@@ -126,7 +132,7 @@ def testconfiguration(self):
self.assertEqual(cfg.get("lbandclientdrat_n"), 2400)
self.assertEqual(cfg.get("userport_s"), "")
self.assertEqual(cfg.get("spartnport_s"), "")
- self.assertEqual(len(cfg.settings), 150)
+ self.assertEqual(len(cfg.settings), 151)
kwargs = {"userport": "/dev/ttyACM0", "spartnport": "/dev/ttyACM1"}
cfg.loadcli(**kwargs)
self.assertEqual(cfg.get("userport_s"), "/dev/ttyACM0")
@@ -140,13 +146,7 @@ def testloadfile(self):
cfg = Configuration(DummyApp())
res = cfg.loadfile("bad.json")
- self.assertEqual(
- res,
- (
- "bad.json",
- "Unrecognised configuration setting 'xcheckforupdate_b: 0'; using defaults",
- ),
- )
+ self.assertEqual(res, ("bad.json", ""))
res = cfg.loadfile("good.json")
self.assertEqual(res, ("good.json", ""))
@@ -915,6 +915,13 @@ def testtrackbounds(self):
self.assertAlmostEqual(center.lat, 53.367617, 7)
self.assertAlmostEqual(center.lon, -1.815437, 7)
+ def testunusedsats(self):
+ gsv_data = {(1,1): (0,0,0,0,5,0),(1,2): (0,0,0,0,7,0),(2,1): (0,0,0,0,0,0),(2,2): (0,0,0,0,1,0),(3,1): (0,0,0,0,5,0)}
+ self.assertEqual(unused_sats(gsv_data), 1)
+ gsv_data = {(1,1): (0,0,0,0,5,0),(1,2): (0,0,0,0,7,0),(2,1): (0,0,0,0,0,0),(2,2): (0,0,0,0,1,0),(3,1): (0,0,0,0,0,0)}
+ self.assertEqual(unused_sats(gsv_data), 2)
+ gsv_data = {(1,1): (0,0,0,0,5,0),(1,2): (0,0,0,0,7,0),(2,1): (0,0,0,0,1,0),(2,2): (0,0,0,0,1,0),(3,1): (0,0,0,0,4,0)}
+ self.assertEqual(unused_sats(gsv_data), 0)
if __name__ == "__main__":
# import sys;sys.argv = ['', 'Test.testName']