diff --git a/INSTALLATION.md b/INSTALLATION.md
index 180c37f4..bfbb6c36 100644
--- a/INSTALLATION.md
+++ b/INSTALLATION.md
@@ -63,7 +63,7 @@ In the following, `python3` & `pip` refer to the Python 3 executables. You may n
- Graphical User Interface (window system / manager)
- Python >= 3.10⁴
-- Tk (tkinter) >= 8.6⁵ (*tkinter is a commonly used library for developing Graphical User Interfaces (GUI) in Python*)
+- Tk (tkinter) >= 8.6⁵⁶ (*tkinter is a commonly used library for developing Graphical User Interfaces (GUI) in Python*)
- Screen resolution >= 640 x 480 (VGA); Ideally 1920 x 1080, though at lower screen resolutions (<= 1024 width), top level dialogs will be resizable and scrollable.
**NB** It is highly recommended to use the latest official [Python.org](https://www.python.org/downloads/) installation package for your platform, rather than any pre-installed version.
@@ -81,18 +81,18 @@ Normally installs without any additional steps.
### MacOS 13 or later
-⁵ The version of Python supplied with some older Apple MacOS platforms includes a [deprecated version of tkinter](https://www.python.org/download/mac/tcltk/) (8.5). Use an official [Python.org](https://www.python.org/downloads/macos) installation package instead.
+**NB:** PyGPSClient does ***NOT*** normally require Homebrew or MacPorts to be installed on MacOS (either Intel or Apple Silicon). The Python organisation provides serviceable [64-bit universal MacOS installation packages](https://www.python.org/downloads/macos/) for all current and legacy versions of Python, including release candidates. Note, however, the following caveats:
-**NB:** Python does ***NOT*** normally require Homebrew or MacPorts to be installed on MacOS. The Python organisation provides serviceable [64-bit universal installation packages](https://www.python.org/downloads/macos/) for all current and legacy versions of Python, including release candidates.
+⁵ The version of Python supplied with some older Apple MacOS platforms includes a [deprecated version of tkinter](https://www.python.org/download/mac/tcltk/) (8.5). Use an official [Python for MacOS](https://www.python.org/downloads/macos) installation package instead.
-However, if you wish to install Python using [Homebrew](https://brew.sh/) to take advantage of certain non-default configurations (*e.g. support for sqlite3 extensions*), use the `python-tk` formula rather than `python`, e.g.
+⁶ The version of tkinter supplied with the very latest official versions of [Python for MacOS](https://www.python.org/downloads/macos) (`>= 3.14.5`), and with Homebrew formula `python-tk >= 3.12`, include a new version of tkinter (9.0). There appear to be some performance and compatibility issues with this version on MacOS Sonoma and Tahoe which may render the PyGPSClient UI somewhat sluggish or unresponsive. Until this is resolved, MacOS users are recommended to use official Python <= 3.14.4 or Homebrew formula `python-tk = 3.11`.
+
+If you wish to install Python using [Homebrew](https://brew.sh/) to take advantage of certain non-default configurations (*e.g. support for sqlite3 extensions*), use the `python-tk` formula rather than `python`, e.g.
```shell
-brew install python-tk@3.13 libspatialite
+brew install python-tk@3.11 libspatialite
```
-Note also that the Homebrew formulae for python-tk>=3.12 include the latest tkinter 9.0 (rather than 8.6). There are known compatibility issues between tkinter 9.0 and other Python packages (*e.g. ImageTk*) on some platform configurations, which may result in PyGPSClient being unable to load. If you encounter these issues, consider using `brew install python-tk@3.11` or an official [Python.org](https://www.python.org/downloads/macos) installation package instead.
-
Note that on MacOS, serial ports may appear as `/dev/tty*` *or* `/dev/cu*`. To understand the differences between the two, see [here](https://www.codegenes.net/blog/what-s-the-difference-between-dev-tty-and-dev-cu-on-macos/).
### Linux (including Raspberry Pi OS)
@@ -128,7 +128,7 @@ Add your user to this group using usermod (*you will need to log out and in agai
sudo usermod -aG dialout myuser
```
-For Debian-based platforms, the group is normally `dialout` or `tty`; on Arch-based platforms it is normally `uucp`.
+For Debian-based platforms, the group is normally `dialout`; on Arch-based platforms it is normally `uucp`.
Other than this, no special privileges are required.
diff --git a/README.md b/README.md
index 12b57370..412a51a2 100644
--- a/README.md
+++ b/README.md
@@ -107,7 +107,8 @@ For more comprehensive installation instructions, please refer to [INSTALLATION.
1. By default, the Settings panel is displayed to the right of the main application window. It can be hidden or shown via Menu..View..Hide/Show Settings. The panel can also be 'undocked' from the main application window via Menu..View..Undock Settings and - if [non-transient](#transient) (`transient_dialog_b: 0`) - minimized independently of the main window. Exiting the undocked dialog, or selecting Menu..View..Dock Settings, will 'dock' the panel.
2. Protocols Shown - Select which message protocols to display in the console; NMEA, UBX (*u-blox binary*), SBF (*Septentrio binary*), UNI (*Unicore binary*), QGC (*Quectel binary*), RTCM3, SPARTN or TTY (*terminal*). NB: this only changes the *displayed* protocols - to change the actual protocols output by the receiver, use the relevant configuration command(s).
- - **NB:** Serial connection must be stopped before changing to or from TTY (terminal) protocol. Enabling TTY (terminal) mode will disable all other protocols.
+ - **NB:** Serial connection must be stopped before changing to or from TTY (terminal) protocol or "Meta Only" mode. Enabling TTY (terminal) mode will disable all other protocols.
+ - If "Meta Only" is enabled, the underlying `GNSSReader` parser will *only* parse basic metadata from each incoming raw message (protocol, identity, length). This mode is vastly quicker (≈ 100x) than full parsing, but individual data attributes *will no longer be available* and PyGPSClient's widgets other than console will *not* be updated.
3. To connect to a GNSS receiver via USB or UART port, select the device from the listbox, set the appropriate serial connection parameters and click
. The application will endeavour to pre-select a recognised GNSS/GPS device but this is platform and device dependent. Press the  button to refresh the list of connected devices at any point.
- `Rate bps` (baud rate) is typically the only setting that might need adjusting, but tweaking the `timeout` setting may improve performance on certain platforms.
@@ -145,6 +146,12 @@ For more comprehensive installation instructions, please refer to [INSTALLATION.
#### Datalogging, GPX Track Recording and Database
17. 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).
+
+ **NB**: For extended datalogging (> 2 hours or more) it is **strongly** recommended to use an unattended (e.g. CLI) tool like [GNSSStreamer](https://github.com/semuconsulting/pygnssutils#gnssstreamer) rather than an attended GUI tool like PyGPSClient (*especially if you're running 'headless'*), e.g.:
+ ```shell
+ gnssstreamer --port /dev/ttyACM0 --baudrate 115200 --timeout 3 --format 2 --clioutput 1 --output pygpsdata.log --verbosity 2
+ ```
+
18. 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. See also [GPX Track Viewer](#gpxviewer).
19. 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 may take up to a minute or so to initialise*.
- Database logging 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 - `no-ext` means the spatialite extension is not supported; `no-ms` means spatialite *is* supported but the necessary `mod_spatialite` extension module cannot be found in the PATH; a numeric version number like `3.51.2` indicates spatialite is fully supported.
@@ -153,31 +160,33 @@ For more comprehensive installation instructions, please refer to [INSTALLATION.
#### Pop-up Configuration Dialogs
-20. [UBX Configuration Dialog](#ubxconfig), with the ability to send a variety of UBX CFG configuration commands to any u-blox GNSS device. 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
+20. [UBX Configuration Dialog](#ubxconfig), with the ability to send a variety of UBX CFG configuration commands to any modern (Generation 9 or later e.g. ZED-F9P, ZED-X20P) u-blox GNSS device. 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 modern UBX GNSS device via serial port*), click
, or go to Menu..Options..UBX Configuration Dialog.
-21. [NMEA Configuration Dialog](#nmeaconfig), with the ability to send a variety of NMEA configuration commands to compatible GNSS devices (e.g. Quectel LG or LC Series). 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.
-22. [TTY Config Dialog](#ttycommands), with the ability to send a variety of TTY (ASCII) configuration commands to compatible GNSS and related devices (e.g. Septentrio Mosaic Series, Unicore UM9** Series). This includes the facility to add **user-defined commands or command sequences** - see instructions under [user-defined presets](#userdefined) below. To display the TTY Configuration Dialog (*only functional when connected to a compatible GNSS device via serial port*), click
+21. [UBX Legacy Configuration Dialog](#ubxconfig), with the ability to send a variety of UBX CFG configuration commands to any legacy (Generation 8 or earlier e.g. NEO-M7P, NEO-M8P) u-blox GNSS device. To display the UBX Legacy Configuration Dialog (*only functional when connected to a legacy UBX GNSS device via serial port*), click
+, or go to Menu..Options..UBX Configuration Dialog.
+22. [NMEA Configuration Dialog](#nmeaconfig), with the ability to send a variety of NMEA configuration commands to compatible GNSS devices (e.g. Quectel LG or LC Series). 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.
+23. [TTY Config Dialog](#ttycommands), with the ability to send a variety of TTY (ASCII) configuration commands to compatible GNSS and related devices (e.g. Septentrio Mosaic Series, Unicore UM9** Series). This includes the facility to add **user-defined commands or command sequences** - see instructions under [user-defined presets](#userdefined) below. To display the TTY Configuration Dialog (*only functional when connected to a compatible GNSS device via serial port*), click
, or go to Menu..Options..TTY Configuration.
-23. [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
+24. [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.
-24. [Server Config](#socketserver) facility with the ability to act as an NTRIP caster (mountpoint = `pygnssutils`) or generic socket server. To display the Server Configuration Dialog, click
+25. [Server Config](#socketserver) facility with the ability to act as an NTRIP caster (mountpoint = `pygnssutils`) or generic socket server. To display the Server Configuration Dialog, click
, or go to Menu..Options..Server Configuration Dialog.
-25. [GPX Track Viewer](#gpxviewer) facility with the ability to map GPX files containing track, route or waypoint data and show elevation and speed profiles and other metadata. To display the GPX Track viewer, go to Menu..Options..GPX Track Viewer.
-26. [RINEX Conversion](#rinex) facility which supports conversion of previously-saved binary datalogs to RINEX observation and navigation format. To display the RINEX Conversion dialog, go to Menu..Options..RINEX Conversion.
-27. [Import Custom Map](#custommap) facility which allows the user to import geo-referenced images for use as background maps. To display the Import Custom Map dialog, go to Menu..Options..Import Custom Map.
-28. [Configuration Command Recorder](#recorder) facility which allows the user to record, save, load, import (*as a preset*) and replay UBX, NMEA or TTY configuration commands sent to a receiver. To display the Command Record Facility dialog, go to Menu..Options..Configuration Command Recorder.
-29. [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.
+26. [GPX Track Viewer](#gpxviewer) facility with the ability to map GPX files containing track, route or waypoint data and show elevation and speed profiles and other metadata. To display the GPX Track viewer, go to Menu..Options..GPX Track Viewer.
+27. [RINEX Conversion](#rinex) facility which supports conversion of previously-saved binary datalogs to RINEX observation and navigation format. To display the RINEX Conversion dialog, go to Menu..Options..RINEX Conversion.
+28. [Import Custom Map](#custommap) facility which allows the user to import geo-referenced images for use as background maps. To display the Import Custom Map dialog, go to Menu..Options..Import Custom Map.
+29. [Configuration Command Recorder](#recorder) facility which allows the user to record, save, load, import (*as a preset*) and replay UBX, NMEA or TTY configuration commands sent to a receiver. To display the Command Record Facility dialog, go to Menu..Options..Configuration Command Recorder.
+30. [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.
#### GUI refresh rate setting
-30. PyGPSClient processes all incoming GNSS data in 'real time' but, by default, the GUI is only refreshed every 0.5 seconds. The refresh rate can be manually configured via the `guiupdateinterval_f` setting in the json configuration file. **NB:** PyGPSClient may become unresponsive on slower platforms (e.g. Raspberry Pi) at high message rates if the GUI update interval is less than 0.1 seconds, though lower intervals (<= 0.1 secs) can be accommodated on more powerful platforms.
+31. PyGPSClient processes all incoming GNSS data in 'real time' but, by default, the GUI is only refreshed every 0.5 seconds. The refresh rate can be manually configured via the `guiupdateinterval_f` setting in the json configuration file. **NB:** PyGPSClient may become unresponsive on slower platforms (e.g. Raspberry Pi) at high message rates if the GUI update interval is less than 0.1 seconds, though lower intervals (<= 0.1 secs) can be accommodated on more powerful platforms.
#### Toplevel ('pop-up') dialog setting
-31. The behaviour of Toplevel ('pop-up') dialogs will depend on the screen resolution and 'transient' setting. If the width or height of a Toplevel dialog exceeds the screen resolution, the dialog will be displayed in a scrollable, resizeable window. Otherwise, the dialog is displayed as a fixed, non-resizeable panel.
+32. The behaviour of Toplevel ('pop-up') dialogs will depend on the screen resolution and 'transient' setting. If the width or height of a Toplevel dialog exceeds the screen resolution, the dialog will be displayed in a scrollable, resizeable window. Otherwise, the dialog is displayed as a fixed, non-resizeable panel.
- A boolean configuration setting `transient_dialog_b` governs whether Toplevel dialogs are 'transient' (i.e. always on top of main application dialog) or not. Changing this setting to `0` allows Toplevel 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 non-transient windows.
- - A boolean configuration setting `resizeable_dialog_b` governs whether *all* Toplevel dialogs are resizeable, irrespective of the default setting in `DialogState`.
+ - A boolean configuration setting `resizeable_dialog_b` governs whether *all* Toplevel dialogs are resizeable, irrespective of the default setting in `DialogState`. Setting this to '1' provides a workaround for issues with some scaled Linux Wayland displays.
#### User-selectable widgets
---
@@ -185,8 +194,8 @@ For more comprehensive installation instructions, please refer to [INSTALLATION.
|---------------------------|---------------------------------------------------------------------------------------------------|
|| 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. Double-click the "hae" label or value to toggle between hae (height above ellipsoid) and sep (separation) values. **NB**: some fields (e.g. hdop/vdop, hacc/vacc) are only available from proprietary NMEA or UBX messages and may not be output by default. The minimum messages required to populate all available fields are: NMEA: GGA, GSA, GSV, RMC, UBX00 (proprietary); UBX: NAV-DOP, NAV-PVT, NAV-SAT; UNI: BESTNAV, SATSINFO, STADOP. |
|| 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). |
-|| Levels view widget showing current satellite carrier-to-noise (C/No) levels for each GNSS constellation. Double-click to toggle legend. Double-right-click to toggle levels where C/No = 0 dbHz. |
+|| Skyview widget showing current satellite visibility and position (elevation / azimuth). Satellite icon borders are colour-coded to distinguish between different GNSS constellations. For consistency between NMEA and UBX data sources, will display GLONASS NMEA SVID (65-96) rather than slot (1-24). A manually-edited boolean configuration parameter `unusedsat_b` determines whether or not to include satellites where CNo = 0; default = 0. |
+|| Levels view widget showing current satellite carrier-to-noise (C/No) levels for each GNSS constellation. Double-click to toggle legend.|
|| Signals view widget showing current svid/signal carrier-to-noise (C/No) level and (where applicable) correction source for each GNSS svid/signal received (*GNSS receiver must be capable of outputting UBX NAV-SIG messages*). Signal identifiers are in RINEX format e.g. `L1_C/A`, `E5_aQ`, etc. Double-click to toggle legend. Double-right-click to toggle signals where C/No = 0 dbHz. |
|| 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. |
@@ -195,7 +204,7 @@ For more comprehensive installation instructions, please refer to [INSTALLATION.
|| 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. |
+| | Relative Position (formerly 'Rover') widget plots relative 2D bearing, distance, track and status information for the following configurations: (i) NAV-RELPOSNED - position of roving receiver relative to a fixed or moving RTK base (including an NTRIP mountpoint); (ii) RTCM 1005/6 - position of roving receiver relative to a fixed RTK NTRIP antenna reference point (ARP); (iii) NAV-DAHEADING - static heading ('yaw') from a dual-antenna receiver e.g. u-blox ZED-X20D. Double-click to clear existing plot. |
| | Chart widget acts as a multi-channel "plotter", allowing the user to plot a series of named numeric data attributes from any parsed GNSS data source, with configurable y (value) and x (time) axes. By default, the number of channels is set to 4, but this can be manually edited by the user via the json configuration file setting `chartsettings_d["numchn_n"]`. For each channel, user can select: (*optional*) identity of message source e.g. `NAV-PVT`; attribute name e.g. `hAcc`; scaling factor (divisor) e.g. 1000; y axis range e.g. 0 - 5. Wildcards are available for attribute groups - "\*" (average of group values), "+" (maximum of group values), "-" (minimum of group values) e.g. `cno*` will plot the average `cno` value for a group of satellites. Double-click to clear the existing data. Double-right-click to save the current chart data to the clipboard in CSV format, which can be directly pasted into a spreadsheet application. |
| | Attitude Monitor widget (*formerly "IMU Monitor"*) showing current orientation/attitude (roll, pitch, yaw *aka 'static heading'*) and status from a variety of IMU, Dead Reckoning, Dual Antenna or other 2D/3D attitude message sources. Select range in degrees (from ±1 to ±180 degrees). |
@@ -519,22 +528,24 @@ For further details, refer to the `pygnssutils` homepage at [https://github.com/
---
## Troubleshooting
-1. If you encounter persistent `WARNING>>Error parsing data stream Serial stream terminated unexpectedly` messages in the console, this may be indicative of insufficient serial port bandwidth (baudrate or timeout) for the current output message cohort (*particularly if this includes Ephemera or Observation data*). Try increasing the baudrate in the first instance.
+1. **NB:** The latest version of Python for MacOS (>=3.14.5) comes with a new version of tkinter (9.0). There appear to be fairly serious performance issues with this version on MacOS Tahoe which render the PyGPSClient UI somewhat sluggish. For the time being, it is recommended that users use >=3.14.4. This issue does *not* affect other operating systems or Python apps not using tkinter.
+
+2. If you encounter persistent `WARNING>>Error parsing data stream Serial stream terminated unexpectedly` messages in the console, this may be indicative of insufficient serial port bandwidth (baudrate or timeout) for the current output message cohort (*particularly if this includes Ephemera or Observation data*). Try increasing the baudrate in the first instance.
-2. Most [budget USB-UART adapters](https://www.amazon.co.uk/DSD-TECH-adapter-FT232RL-Compatible/dp/B07BBPX8B8?ref_=ast_sto_dp) (e.g. FT232, CH345, CP2102, *including those embedded on development boards*) have a bandwidth limit of around 3Mbps (≈ 375000 baud) and may not work reliably above 230600 baud, even if the receiver supports higher baud rates. If you're using an adapter and notice significant message corruption (e.g. frequent `WARNING>>..invalid checksum` messages), try reducing the baud rate to a maximum 230600.
+3. Most [budget USB-UART adapters](https://www.amazon.co.uk/DSD-TECH-adapter-FT232RL-Compatible/dp/B07BBPX8B8?ref_=ast_sto_dp) (e.g. FT232, CH345, CP2102, *including those embedded on development boards*) have a bandwidth limit of around 3Mbps (≈ 375000 baud) and may not work reliably above 230600 baud, even if the receiver supports higher baud rates. If you're using an adapter and notice significant message corruption (e.g. frequent `WARNING>>..invalid checksum` messages), try reducing the baud rate to a maximum 230600.
-3. Some Linux Wayland platforms appear to require Toplevel dialog windows to be non-transient (`transient_dialog_b: 0`) for the window 'maximise' icon to work properly.
+4. Some Linux Wayland platforms appear to require Toplevel dialog windows to be non-transient (`transient_dialog_b: 0`) for the window 'maximise' icon to work properly.
-4. Some Homebrew-installed Python environments on MacOS can give rise to critical segmentation errors (*illegal memory access*) when shell subprocesses are invoked, due to the way permissions are implemented. This may, for example, affect About..Update functionality; the workaround is to update via a standard CLI `pip install --upgrade` command.
+5. Some Homebrew-installed Python environments on MacOS can give rise to critical segmentation errors (*illegal memory access*) when shell subprocesses are invoked, due to the way permissions are implemented. This may, for example, affect About..Update functionality; the workaround is to update via a standard CLI `pip install --upgrade` command.
-5. Installing the optional `cryptography` package on some 32-bit Linux platforms (e.g. Raspberry Pi OS 32) may require [Rust compiler support](https://www.rust-lang.org/tools/install) and some [additional build dependencies](https://cryptography.io/en/latest/installation/) (see [pyspartn cryptography installation notes](https://github.com/semuconsulting/pyspartn/tree/main/cryptography_installation#readme)):
+6. Installing the optional `cryptography` package on some 32-bit Linux platforms (e.g. Raspberry Pi OS 32) may require [Rust compiler support](https://www.rust-lang.org/tools/install) and some [additional build dependencies](https://cryptography.io/en/latest/installation/) (see [pyspartn cryptography installation notes](https://github.com/semuconsulting/pyspartn/tree/main/cryptography_installation#readme)):
```shell
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
sudo apt-get install build-essential libssl-dev libffi-dev python3-dev pkg-config
```
-6. As of October 2025, u-blox have [discontinued their PointPerfect SPARTN L-Band and MQTT services](https://portal.u-blox.com/s/question/0D5Oj00000uB53GKAS/suspension-of-european-pointperfect-lband-spartn-service). As a result, PyGPSClient's [SPARTN Configuration](#spartnconfig) panel is largely redundant and is disabled by default in version>=1.5.17, though it can be re-enabled by manually setting the `lband_enabled_b` configuration setting to 1.
+7. As of October 2025, u-blox have [discontinued their PointPerfect SPARTN L-Band and MQTT services](https://portal.u-blox.com/s/question/0D5Oj00000uB53GKAS/suspension-of-european-pointperfect-lband-spartn-service). As a result, PyGPSClient's [SPARTN Configuration](#spartnconfig) panel is largely redundant and is disabled by default in version>=1.5.17, though it can be re-enabled by manually setting the `lband_enabled_b` configuration setting to 1.
---
## License
diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md
index 18ce089b..aa436c10 100644
--- a/RELEASE_NOTES.md
+++ b/RELEASE_NOTES.md
@@ -1,5 +1,21 @@
# PyGPSClient Release Notes
+### RELEASE 1.7.1
+
+FIXES:
+
+1. Fix 'memory leak' issue (socket server message queue filling up when server not active).
+1. Internal refactoring and performance enhancements to improve long-term UI responsiveness and stability.
+1. Fix u-blox preset configuration validation which was preventing some valid preset commands.
+
+ENHANCEMENTS:
+
+1. Split UBX Configuration panel into two separate panels - one for modern (Gen9+) receivers and one for legacy (Gen8-) receivers. The intention is to make the panels easier to use on lower-resolution or scaled displays.
+1. Display UBX NAV-DAHEADING data from dual-antenna receivers such as the u-blox ZED-X20D in the "Rover Plot" widget.
+1. Update portid designation in System Monitor I/O widget (X20 assigns different MON-COMMS.portids to F9P).
+1. Add satellite filter entry field to RINEX conversion dialog (requires pygnssutils>=1.2.6).
+1. "Meta Only" parsing mode added. When enabled, incoming raw data will *only* be parsed for basic metadata (protocol, identity, length) rather than individual data attributes. This mode is significantly quicker than full parsing, but individual data attributes *will no longer be available* and PyGPSClient's widgets other than the console will *not* be updated. Utilises the new [GNSSReader](https://github.com/semuconsulting/pygnssutils#gnssreader) `parsing=2` argument. Requires pygnssutils>=1.2.6.
+
### RELEASE 1.7.0
1. Can now double-click to toggle between 'hae' (height above ellipsoid) and 'sep' (separation) values in banner panel.
diff --git a/docs/pygpsclient.rst b/docs/pygpsclient.rst
index 7e32acb9..9da9c236 100644
--- a/docs/pygpsclient.rst
+++ b/docs/pygpsclient.rst
@@ -508,6 +508,14 @@ pygpsclient.ubx\_handler module
:undoc-members:
:show-inheritance:
+pygpsclient.ubx\_legacy\_config\_dialog module
+----------------------------------------------
+
+.. automodule:: pygpsclient.ubx_legacy_config_dialog
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
pygpsclient.ubx\_msgrate\_frame module
--------------------------------------
diff --git a/images/rover_widget.png b/images/rover_widget.png
index 904f105c..2d6a1301 100644
Binary files a/images/rover_widget.png and b/images/rover_widget.png differ
diff --git a/pygpsclient.json b/pygpsclient.json
index cfa52fc5..28a61a1c 100644
--- a/pygpsclient.json
+++ b/pygpsclient.json
@@ -47,8 +47,8 @@
"gpxmapzoom_n": 10,
"gpxtype_s": "track",
"showtrack_b": 0,
- "legend_b": 1,
- "unusedsat_b": 0,
+ "legend_b": true,
+ "unusedsat_b": false,
"datalog_b": 0,
"logformat_s": "Binary",
"logpath_s": "",
diff --git a/pyproject.toml b/pyproject.toml
index 776f1d65..0585cf6a 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -52,10 +52,10 @@ classifiers = [
dependencies = [
"requests>=2.34.0",
"Pillow>=12.0.0",
- "pygnssutils>=1.2.5",
+ "pygnssutils>=1.2.5", # TODO requires >=1.2.6 when available
"pyunigps>=1.0.0",
"pynmeagps>=1.1.5",
- "pyubx2>=1.3.3",
+ "pyubx2>=1.3.5",
]
[project.scripts]
diff --git a/src/pygpsclient/__main__.py b/src/pygpsclient/__main__.py
index fc951336..b243c121 100644
--- a/src/pygpsclient/__main__.py
+++ b/src/pygpsclient/__main__.py
@@ -10,7 +10,6 @@
from argparse import SUPPRESS, ArgumentDefaultsHelpFormatter, ArgumentParser
from logging import getLogger
-from tkinter import Tk
from pygnssutils import (
VERBOSITY_CRITICAL,
@@ -118,8 +117,8 @@ def main():
f"{VERBOSITY_CRITICAL} = critical, "
f"{VERBOSITY_LOW} = low (error), "
f"{VERBOSITY_MEDIUM} = medium (warning), "
- f"{VERBOSITY_HIGH} = high (info), {VERBOSITY_DEBUG} = debug, "
- f"default = {VERBOSITY_CRITICAL}"
+ f"{VERBOSITY_HIGH} = high (info), "
+ f"{VERBOSITY_DEBUG} = debug"
),
type=int,
choices=[
@@ -129,7 +128,7 @@ def main():
VERBOSITY_DEBUG,
VERBOSITY_CRITICAL,
],
- default=VERBOSITY_LOW,
+ default=VERBOSITY_CRITICAL,
)
ap.add_argument(
"--logtofile",
@@ -139,7 +138,7 @@ def main():
kwargs = vars(ap.parse_args())
# set up global logging configuration
- verbosity = int(kwargs.pop("verbosity", VERBOSITY_LOW))
+ verbosity = int(kwargs.pop("verbosity", VERBOSITY_CRITICAL))
logtofile = kwargs.pop("logtofile", "")
logger = getLogger(APPNAME) # "pygpsclient"
logger_utils = getLogger("pygnssutils")
@@ -147,10 +146,10 @@ def main():
for logr in (logger, logger_utils, logger_pyubx2):
set_logging(logr, verbosity, logtofile)
- root = Tk()
- App(root, **kwargs)
- root.mainloop()
+ app = App(**kwargs)
+ app.mainloop()
if __name__ == "__main__":
+
main()
diff --git a/src/pygpsclient/_version.py b/src/pygpsclient/_version.py
index 32122df4..07f89dde 100644
--- a/src/pygpsclient/_version.py
+++ b/src/pygpsclient/_version.py
@@ -8,4 +8,4 @@
:license: BSD 3-Clause
"""
-__version__ = "1.7.0"
+__version__ = "1.7.1"
diff --git a/src/pygpsclient/about_dialog.py b/src/pygpsclient/about_dialog.py
index 6a764161..4e74ee2f 100644
--- a/src/pygpsclient/about_dialog.py
+++ b/src/pygpsclient/about_dialog.py
@@ -55,11 +55,10 @@ def __init__(self, app, *args, **kwargs): # pylint: disable=unused-argument
"""
Initialise Toplevel dialog
- :param Frame app: reference to main tkinter application
+ :param Tk app: reference to main tkinter application
"""
self.__app = app # Reference to main application class
- self.__master = self.__app.appmaster # Reference to root class (Tk)
self.logger = logging.getLogger(__name__)
self._img_icon = ImageTk.PhotoImage(Image.open(ICON_APP128).resize((64, 64)))
self._img_github = ImageTk.PhotoImage(Image.open(ICON_GITHUB).resize((32, 32)))
@@ -190,7 +189,7 @@ def _on_github(self, *args, **kwargs): # pylint: disable=unused-argument
"""
if brew_installed():
- self.status_label = (BREWWARN, INFOCOL)
+ self.set_status_label(BREWWARN, INFOCOL)
return
open_new_tab(GITHUB_URL)
@@ -202,7 +201,7 @@ def _on_sponsor(self, *args, **kwargs): # pylint: disable=unused-argument
"""
if brew_installed():
- self.status_label = (BREWWARN, INFOCOL)
+ self.set_status_label(BREWWARN, INFOCOL)
return
open_new_tab(SPONSOR_URL)
@@ -214,7 +213,7 @@ def _on_license(self, *args, **kwargs): # pylint: disable=unused-argument
"""
if brew_installed():
- self.status_label = (BREWWARN, INFOCOL)
+ self.set_status_label(BREWWARN, INFOCOL)
return
open_new_tab(LICENSE_URL)
@@ -225,7 +224,7 @@ def _check_for_update(self, *args, **kwargs): # pylint: disable=unused-argument
Check for updates.
"""
- self.status_label = ("Checking for updates...", INFOCOL)
+ self.set_status_label("Checking for updates...", INFOCOL)
versions = check_for_updates()
for i, (nam, current, latest) in enumerate(versions):
txt = f"{nam}: {current}"
@@ -238,14 +237,14 @@ def _check_for_update(self, *args, **kwargs): # pylint: disable=unused-argument
else:
txt += f" - Latest version is {latest}"
col = ERRCOL
- self._lbl_lib_versions[i].config(text=txt, fg=col)
-
+ self._lbl_lib_versions[i]["text"] = txt
+ self._lbl_lib_versions[i]["fg"] = col
updates = [nam for (nam, current, latest) in versions if latest != current]
if len(updates) > 0:
- self.status_label = ("Updates available", OKCOL)
+ self.set_status_label("Updates available", OKCOL)
self._set_update_btn_mode(True)
else:
- self.status_label = ("No updates available", INFOCOL)
+ self.set_status_label("No updates available", INFOCOL)
def _do_update(self, *args, **kwargs): # pylint: disable=unused-argument
"""
@@ -253,15 +252,15 @@ def _do_update(self, *args, **kwargs): # pylint: disable=unused-argument
"""
if brew_installed():
- self.status_label = (BREWUPDATE, INFOCOL)
+ self.set_status_label(BREWUPDATE, INFOCOL)
return
- self.status_label = (UPDATEINPROG, INFOCOL)
+ self.set_status_label(UPDATEINPROG, INFOCOL)
rc = self.__app.do_app_update()
if rc:
- self.status_label = (UPDATERESTART, OKCOL)
+ self.set_status_label(UPDATERESTART, OKCOL)
else:
- self.status_label = (UPDATEERR.format(err=rc), ERRCOL)
+ self.set_status_label(UPDATEERR.format(err=rc), ERRCOL)
self._set_update_btn_mode(False)
def _set_update_btn_mode(self, update: bool):
@@ -272,9 +271,11 @@ def _set_update_btn_mode(self, update: bool):
"""
if update:
- self._btn_checkupdate.config(text="UPDATE", fg=OKCOL)
+ self._btn_checkupdate["text"] = "UPDATE"
+ self._btn_checkupdate["fg"] = OKCOL
self._btn_checkupdate.bind("", self._do_update)
else:
- self._btn_checkupdate.config(text="CHECK FOR UPDATES", fg=INFOCOL)
+ self._btn_checkupdate["text"] = "CHECK FOR UPDATES"
+ self._btn_checkupdate["fg"] = INFOCOL
self._btn_checkupdate.bind("", self._check_for_update)
- self.update_idletasks()
+ self.__app.update_idletasks()
diff --git a/src/pygpsclient/app.py b/src/pygpsclient/app.py
index efed2224..c36816ca 100644
--- a/src/pygpsclient/app.py
+++ b/src/pygpsclient/app.py
@@ -37,17 +37,16 @@
# pylint: disable=no-member
import logging
-from datetime import datetime, timedelta
from inspect import currentframe, getfile
-from os import path
+from os import getenv, path
from queue import Empty, Queue
from subprocess import CalledProcessError, run
from sys import executable
from threading import Thread
-from tkinter import EW, NSEW, NW, Frame, Label, PhotoImage, Tk, Toplevel, font
+from tkinter import EW, NSEW, NW, Frame, PhotoImage, Tk, Toplevel, font
from types import NoneType
-from pygnssutils import GNSSMQTTClient, GNSSNTRIPClient, MQTTMessage
+from pygnssutils import GNSSMQTTClient, GNSSNTRIPClient
from pygnssutils.gnssreader import (
NMEA_PROTOCOL,
POLL,
@@ -59,12 +58,9 @@
)
from pygnssutils.socket_server import ClientHandler, ClientHandlerTLS, SocketServer
from pynmeagps import NMEAMessage
-from pyqgc import QGCMessage
from pyrtcm import RTCMMessage
-from pysbf2 import SBFMessage
from pyspartn import SPARTNMessage
from pyubx2 import UBXMessage
-from pyunigps import UNIMessage
from serial import SerialException, SerialTimeoutException
from pygpsclient._version import __version__ as VERSION
@@ -78,7 +74,6 @@
CMDINITDELAY,
CMDPAUSE,
CONFIGFILE,
- CONNECTED_NTRIP,
CONNECTED_SPARTNIP,
CONNECTED_SPARTNLB,
DISCONNECTED,
@@ -86,7 +81,6 @@
FRAME,
GNSS_EOF_EVENT,
GNSS_ERR_EVENT,
- GNSS_EVENT,
GNSS_TIMEOUT_EVENT,
ICON_APP128,
INFOCOL,
@@ -141,7 +135,6 @@
UPDATEINPROG,
UPDATERESTART,
VERCHECK,
- WARNING,
)
from pygpsclient.tty_handler import TTYHandler
from pygpsclient.ubx_handler import UBXHandler
@@ -155,18 +148,22 @@
SHOW,
UNDOCK,
VISIBLE,
- WDGCHART,
- WDGCONSOLE,
WidgetState,
)
+PSSTATS = False
+if int(getenv("PYGPSCLIENT_PSSTATS", "0")) & 2:
+ import tracemalloc
-class App(Frame):
+ PSSTATS = True
+
+
+class App(Tk):
"""
Main PyGPSClient GUI Application Class.
"""
- def __init__(self, master, **kwargs):
+ def __init__(self, **kwargs):
"""
Set up main application and add frames.
@@ -175,12 +172,13 @@ def __init__(self, master, **kwargs):
:param kwargs: optional (CLI) kwargs
"""
- self.__master = master
self.logger = logging.getLogger(__name__)
- super().__init__(master)
+ super().__init__()
# load config from json file
+ self.refresh_widget_timer = None
+ self.status_msg_timer = None
self._deferredmsg = None
self.widget_state = WidgetState() # widget state
self.file_handler = FileHandler(self)
@@ -195,20 +193,20 @@ def __init__(self, master, **kwargs):
# setup main application window
geom = self.configuration.get("screengeom_s")
if geom == "":
- geom = set_geom(master, MAINSCALE)
- self.__master.geometry(geom)
- self.__master.protocol("WM_DELETE_WINDOW", self.on_exit)
- self.__master.title(TITLE)
- self.__master.iconphoto(True, PhotoImage(file=ICON_APP128))
+ geom = set_geom(self, MAINSCALE)
+ self.geometry(geom)
+ self.protocol("WM_DELETE_WINDOW", self.on_exit)
+ self.title(TITLE)
+ self.iconphoto(True, PhotoImage(file=ICON_APP128))
self._server_status = -1 # socket server status -1 = inactive
- self.gnss_inqueue = Queue() # messages from GNSS receiver
self.gnss_outqueue = Queue() # messages to GNSS receiver
self.ntrip_inqueue = Queue() # messages from NTRIP source
self.spartn_inqueue = Queue() # messages from SPARTN correction rcvr
self.spartn_outqueue = Queue() # messages to SPARTN correction rcvr
self.socket_inqueue = Queue() # message from socket
self.socket_outqueue = Queue() # message to socket
+ self.console_outqueue = Queue() # message to console
self.dialog_state = DialogState() # dialog state
self.gnss_status = GNSSStatus() # holds latest GNSS readings
self.stream_handler = StreamHandler(self)
@@ -226,9 +224,6 @@ def __init__(self, master, **kwargs):
self.frm_settings = None
self._conn_status = DISCONNECTED
self._rtk_conn_status = DISCONNECTED
- now = datetime.now()
- self._last_gui_update = now
- self._last_status_update = now
self._socket_thread = None
self._socket_server = None
self.consoledata = []
@@ -253,18 +248,24 @@ def __init__(self, master, **kwargs):
# display initial connection status
self.frm_banner.update_conn_status(DISCONNECTED)
+ self.set_conn_label(NOTCONN)
+ self.set_device_label(NA)
if self.frm_settings.frm_serial.status == NOPORTS:
- self.status_label = (INTROTXTNOPORTS, ERRCOL)
+ self.set_status_label(INTROTXTNOPORTS, ERRCOL)
# display any deferred messages
if isinstance(self._deferredmsg, tuple):
- self.status_label = self._deferredmsg
+ self.set_status_label(self._deferredmsg)
self._deferredmsg = None
# check for more recent version (if enabled)
if self.configuration.get("checkforupdate_b") and configerr == "":
self._check_update()
+ if PSSTATS:
+ tracemalloc.start() # pylint: disable=possibly-used-before-assignment
+ self.after(0, self.memory_monitor)
+
def _body(self):
"""
Set up frame and widgets.
@@ -272,16 +273,13 @@ def _body(self):
self._set_default_fonts()
self.menu = MenuBar(self)
- self.__master.config(menu=self.menu, bg=BGCOL)
+ self["menu"] = self.menu
+ self["bg"] = BGCOL
- self.frm_banner = BannerFrame(
- self, self.__master, borderwidth=1, relief="groove", bg=BGCOL
- )
- self.frm_status = StatusFrame(
- self, self.__master, borderwidth=1, relief="groove", bg=BGCOL
- )
- self.frm_settings = SettingsFrame(self, self.__master)
- self.frm_widgets = Frame(self.__master, bg=BGCOL)
+ self.frm_banner = BannerFrame(self, borderwidth=1, relief="groove", bg=BGCOL)
+ self.frm_status = StatusFrame(self, borderwidth=1, relief="groove", bg=BGCOL)
+ self.frm_settings = SettingsFrame(self)
+ self.frm_widgets = Frame(self, bg=BGCOL)
def _do_layout(self):
"""
@@ -352,8 +350,8 @@ def _do_layout(self):
col += c
# set column and row weights to control 'pack' behaviour of main layout
- self.__master.grid_columnconfigure(0, weight=1)
- self.__master.grid_rowconfigure(1, weight=1)
+ self.grid_columnconfigure(0, weight=1)
+ self.grid_rowconfigure(1, weight=1)
wcol, wrow = self.frm_widgets.grid_size()
for col in range(wcol):
w = 1 if col < wcolspan else 0
@@ -367,14 +365,13 @@ def _attach_events(self):
Bind events to main application.
"""
- self.__master.bind(GNSS_EVENT, self.on_gnss_read)
- self.__master.bind(GNSS_EOF_EVENT, self.on_gnss_eof)
- self.__master.bind(GNSS_TIMEOUT_EVENT, self.on_gnss_timeout)
- self.__master.bind(GNSS_ERR_EVENT, self.on_stream_error)
- self.__master.bind(NTRIP_EVENT, self.on_ntrip_read)
- self.__master.bind(SPARTN_EVENT, self.on_spartn_read)
- self.__master.bind_all("", self.on_exit)
- self.__master.bind_all("", self.on_killswitch)
+ self.bind(GNSS_EOF_EVENT, self.on_gnss_eof)
+ self.bind(GNSS_TIMEOUT_EVENT, self.on_gnss_timeout)
+ self.bind(GNSS_ERR_EVENT, self.on_stream_error)
+ self.bind(NTRIP_EVENT, self.on_ntrip_read)
+ self.bind(SPARTN_EVENT, self.on_spartn_read)
+ self.bind_all("", self.on_exit)
+ self.bind_all("", self.on_killswitch)
# also bound in check_updates
def settings_toggle(self):
@@ -403,7 +400,7 @@ def settings_dock(self):
if self.dialog_state.state[DLGTSETTINGS][DLG] is not None:
self.dialog_state.state[DLGTSETTINGS][DLG].destroy()
self.dialog_state.state[DLGTSETTINGS][DLG] = None
- self.frm_settings = SettingsFrame(self, self.__master)
+ self.frm_settings = SettingsFrame(self)
else:
if self.dialog_state.state[DLGTSETTINGS][DLG] is None:
if isinstance(self.frm_settings, SettingsFrame):
@@ -514,7 +511,7 @@ def set_event(self, evt: str):
:param str evt: event type string
"""
- self.__master.event_generate(evt)
+ self.event_generate(evt)
def load_config(self):
"""
@@ -523,9 +520,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.status_label = ("", OKCOL)
+ self.set_status_label("", OKCOL)
else:
- self.status_label = (DLGSTOPRTK, ERRCOL)
+ self.set_status_label(DLGSTOPRTK, ERRCOL)
return
_, err = self.configuration.loadfile()
@@ -547,15 +544,15 @@ def save_config(self):
"""
# save current screen geometry
- self.configuration.set("screengeom_s", self.__master.geometry())
+ self.configuration.set("screengeom_s", self.geometry())
err = self.configuration.savefile()
if err == "":
- self.status_label = (SAVECONFIGOK, OKCOL)
+ self.set_status_label(SAVECONFIGOK, OKCOL)
elif err == "cancelled":
pass
else: # save failed
- self.status_label = (SAVECONFIGBAD.format(err), ERRCOL)
+ self.set_status_label(SAVECONFIGBAD.format(err), ERRCOL)
def update_widgets(self):
"""
@@ -570,23 +567,27 @@ def update_widgets(self):
vis = self.configuration.get(key)
vals[VISIBLE] = vis
except KeyError as err:
- self.status_label = (CONFIGERR.format(err), ERRCOL)
+ self.set_status_label(CONFIGERR.format(err), ERRCOL)
- def _refresh_widgets(self):
+ def refresh_widgets(self):
"""
+ TIMER PROCESS WHILE CONNECTED
Refresh visible widgets.
"""
self.frm_banner.update_frame()
- for wdg, wdgdata in self.widget_state.state.items():
+ for _, wdgdata in self.widget_state.state.items():
frm = getattr(self, wdgdata[FRAME], None)
if frm is not None:
if hasattr(frm, "update_frame") and wdgdata[VISIBLE]:
- if wdg == WDGCONSOLE:
- frm.update_frame(self.consoledata)
- self.consoledata = []
- else:
- frm.update_frame()
+ frm.update_frame()
+ self.update()
+
+ if self.conn_status != DISCONNECTED or self.rtk_conn_status != DISCONNECTED:
+ update_interval = int(self.configuration.get("guiupdateinterval_f") * 1000)
+ self.refresh_widget_timer = self.after(
+ update_interval, self.refresh_widgets
+ )
def start_dialog(self, dlg: str):
"""
@@ -598,6 +599,7 @@ def start_dialog(self, dlg: str):
if self.dialog_state.state[dlg][DLG] is None:
cls = self.dialog_state.state[dlg][CLASS]
self.dialog_state.state[dlg][DLG] = cls(self)
+ self.update()
def dialog(self, dlg: str) -> Toplevel:
"""
@@ -699,7 +701,7 @@ def _sockserver_thread(
) as self._socket_server:
self._socket_server.serve_forever()
except OSError as err:
- self.status_label = (f"Error starting socket server {err}", ERRCOL)
+ self.set_status_label(f"Error starting socket server {err}", ERRCOL)
def update_clients(self, clients: int):
"""
@@ -728,7 +730,7 @@ def on_exit(self, *args, **kwargs): # pylint: disable=unused-argument
"""
self._shutdown()
- self.__master.destroy()
+ self.destroy()
def on_killswitch(self, *args, **kwargs): # pylint: disable=unused-argument
"""
@@ -745,28 +747,9 @@ def on_killswitch(self, *args, **kwargs): # pylint: disable=unused-argument
self.rtk_conn_status = DISCONNECTED
except Exception as err: # pylint: disable=broad-exception-caught
self.logger.error(err)
- self.status_label = (KILLSWITCH, ERRCOL)
+ self.set_status_label(KILLSWITCH, ERRCOL)
self.logger.debug(KILLSWITCH)
- def on_gnss_read(self, event): # pylint: disable=unused-argument
- """
- EVENT TRIGGERED
- Action on <> event - data available on GNSS queue.
-
- :param event event: read event
- """
-
- try:
- raw_data, parsed_data = self.gnss_inqueue.get(False)
- if raw_data is not None and parsed_data is not None:
- self.process_data(raw_data, parsed_data)
- # if socket server is running, output raw data to socket
- if self.server_status:
- self.socket_outqueue.put(raw_data)
- self.gnss_inqueue.task_done()
- except Empty:
- pass
-
def on_gnss_eof(self, event): # pylint: disable=unused-argument
"""
EVENT TRIGGERED
@@ -776,9 +759,9 @@ def on_gnss_eof(self, event): # pylint: disable=unused-argument
"""
self.server_status = -1
- self._refresh_widgets()
+ # self._refresh_widgets()
self.conn_status = DISCONNECTED
- self.status_label = (ENDOFFILE, ERRCOL)
+ self.set_status_label(ENDOFFILE, ERRCOL)
def on_gnss_timeout(self, event): # pylint: disable=unused-argument
"""
@@ -789,9 +772,9 @@ def on_gnss_timeout(self, event): # pylint: disable=unused-argument
"""
self.server_status = -1
- self._refresh_widgets()
+ # self._refresh_widgets()
self.conn_status = DISCONNECTED
- self.status_label = (INACTIVE_TIMEOUT, ERRCOL)
+ self.set_status_label(INACTIVE_TIMEOUT, ERRCOL)
def on_stream_error(self, event): # pylint: disable=unused-argument
"""
@@ -801,7 +784,7 @@ def on_stream_error(self, event): # pylint: disable=unused-argument
:param event event: <> event
"""
- self._refresh_widgets()
+ # self._refresh_widgets()
self.conn_status = DISCONNECTED
def on_ntrip_read(self, event): # pylint: disable=unused-argument
@@ -819,21 +802,22 @@ def on_ntrip_read(self, event): # pylint: disable=unused-argument
and parsed_data is not None
and isinstance(raw_data, bytes)
):
- if self._rtk_conn_status == CONNECTED_NTRIP:
- source = "NTRIP"
- else:
- source = "OTHER"
- if isinstance(parsed_data, (RTCMMessage, SPARTNMessage)):
+ if isinstance(parsed_data, RTCMMessage):
+ self.send_to_device(raw_data)
+ if self.protocol_mask & RTCM3_PROTOCOL:
+ self.console_outqueue.put((raw_data, parsed_data, "NTRIP>>"))
+ elif isinstance(parsed_data, SPARTNMessage):
self.send_to_device(raw_data)
- self.process_data(raw_data, parsed_data, source + ">>")
+ if self.protocol_mask & SPARTN_PROTOCOL:
+ self.console_outqueue.put((raw_data, parsed_data, "NTRIP>>"))
elif isinstance(parsed_data, NMEAMessage):
- # i.e. NMEA GGA sentence sent to NTRIP server
- self.process_data(raw_data, parsed_data, source + "<<")
+ if self.protocol_mask & NMEA_PROTOCOL:
+ self.console_outqueue.put((raw_data, parsed_data, "NTRIP<<"))
self.ntrip_inqueue.task_done()
except Empty:
pass
except (SerialException, SerialTimeoutException) as err:
- self.status_label = (f"Error sending to device {err}", ERRCOL)
+ self.set_status_label(f"Error sending to device {err}", ERRCOL)
def on_spartn_read(self, event): # pylint: disable=unused-argument
"""
@@ -848,22 +832,18 @@ def on_spartn_read(self, event): # pylint: disable=unused-argument
if raw_data is not None and parsed_data is not None:
self.send_to_device(raw_data)
if self._rtk_conn_status == CONNECTED_SPARTNLB:
- source = "LBAND"
+ source = "LBAND>>"
elif self._rtk_conn_status == CONNECTED_SPARTNIP:
- source = "MQTT"
+ source = "MQTT>>"
else:
- source = "OTHER"
- self.process_data(
- raw_data,
- parsed_data,
- source + ">>",
- )
+ source = "OTHER>>"
+ self.console_outqueue.put((raw_data, parsed_data, source))
self.spartn_inqueue.task_done()
except Empty:
pass
except (SerialException, SerialTimeoutException) as err:
- self.status_label = (f"Error sending to device {err}", ERRCOL)
+ self.set_status_label(f"Error sending to device {err}", ERRCOL)
def update_ntrip_status(self, status: bool, msgt: tuple | NoneType = None):
"""
@@ -904,98 +884,6 @@ def get_coordinates(self) -> dict:
"diffstation": self.gnss_status.diff_station,
}
- def process_data(self, raw_data: bytes, parsed_data: object, marker: str = ""):
- """
- 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>>"
- """
-
- # self.logger.debug(f"data received {parsed_data.identity}")
- msgprot = 0
- protfilter = self.protocol_mask
- if isinstance(parsed_data, NMEAMessage):
- msgprot = NMEA_PROTOCOL
- elif isinstance(parsed_data, SBFMessage):
- msgprot = SBF_PROTOCOL
- elif isinstance(parsed_data, QGCMessage):
- msgprot = QGC_PROTOCOL
- elif isinstance(parsed_data, UNIMessage):
- msgprot = UNI_PROTOCOL
- elif isinstance(parsed_data, UBXMessage):
- msgprot = UBX_PROTOCOL
- elif isinstance(parsed_data, RTCMMessage):
- msgprot = RTCM3_PROTOCOL
- elif isinstance(parsed_data, SPARTNMessage):
- msgprot = SPARTN_PROTOCOL
- elif isinstance(parsed_data, MQTTMessage):
- msgprot = MQTT_PROTOCOL
- elif isinstance(parsed_data, str):
- if self.configuration.get("ttyprot_b"):
- msgprot = TTY_PROTOCOL
- else:
- marker = WARNING
-
- if msgprot == UBX_PROTOCOL and msgprot & protfilter:
- self.ubx_handler.process_data(raw_data, parsed_data)
- elif msgprot == SBF_PROTOCOL and msgprot & protfilter:
- self.sbf_handler.process_data(raw_data, parsed_data)
- elif msgprot == QGC_PROTOCOL and msgprot & protfilter:
- self.qgc_handler.process_data(raw_data, parsed_data)
- elif msgprot == UNI_PROTOCOL and msgprot & protfilter:
- self.uni_handler.process_data(raw_data, parsed_data)
- elif msgprot == NMEA_PROTOCOL and msgprot & protfilter:
- self.nmea_handler.process_data(raw_data, parsed_data)
- elif msgprot == RTCM3_PROTOCOL and msgprot & protfilter:
- self.rtcm_handler.process_data(raw_data, parsed_data)
- elif msgprot == TTY_PROTOCOL and msgprot & protfilter:
- self.tty_handler.process_data(raw_data, parsed_data)
- elif msgprot == SPARTN_PROTOCOL and msgprot & protfilter:
- pass
- elif msgprot == MQTT_PROTOCOL:
- pass
-
- # 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
- )
-
- # update consoledata if console is visible and protocol not filtered
- if self.widget_state.state[WDGCONSOLE][VISIBLE] and (
- msgprot in (0, MQTT_PROTOCOL) or msgprot & protfilter
- ):
- self.consoledata.append((raw_data, parsed_data, marker))
-
- # periodically update widgets if visible
- now = datetime.now()
- if now > self._last_gui_update + timedelta(
- seconds=self.configuration.get("guiupdateinterval_f")
- ):
- self._refresh_widgets()
- # update database if enabled
- if self.configuration.get("database_b"):
- self.sqlite_handler.load_data()
- self._last_gui_update = datetime.now()
-
- # remove stale status messages
- if now > self._last_status_update + timedelta(seconds=STATUS_TIMEOUT):
- self.status_label = ("", INFOCOL)
-
- # update GPX track file if enabled
- if self.configuration.get("recordtrack_b"):
- self.file_handler.update_gpx_track()
-
- # update log file if enabled
- if self.configuration.get("datalog_b"):
- self.file_handler.write_logfile(raw_data, parsed_data)
-
- self.update_idletasks()
-
def send_to_device(
self, data: bytes | list[bytes], pause: int = 0, interval: int = 0
):
@@ -1031,13 +919,13 @@ def _check_update(self):
hotkey = ""
else:
hotkey = " CTRL-U to update."
- self.__master.bind_all("", self.do_app_update)
- self.status_label = (
+ self.bind_all("", self.do_app_update)
+ self.set_status_label(
VERCHECK.format(title=TITLE, version=latest, hotkey=hotkey),
ERRCOL,
)
else:
- self.__master.unbind("")
+ self.unbind("")
def poll_version(self, protocol: int):
"""
@@ -1046,7 +934,7 @@ def poll_version(self, protocol: int):
:param int protocol: protocol(s)
"""
- self.device_label = NA
+ self.set_device_label(NA)
msgs = []
if protocol & UBX_PROTOCOL:
msgs.append(UBXMessage("MON", "MON-VER", POLL).serialize())
@@ -1066,85 +954,40 @@ def poll_version(self, protocol: int):
interval=self.configuration.get("ttydelay_b") * CMDPAUSE,
)
- @property
- def appmaster(self) -> Tk:
- """
- Getter for application master (Tk).
-
- :return: reference to master Tk instance
- :rtype: 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]):
+ def set_conn_label(self, msg: str, col: str = INFOCOL):
"""
Sets connection description in status bar.
- :param str | tuple connection: (connection, color)
+ :param str msg: connection descriptor
+ :param str col: color
"""
- if isinstance(connection, tuple):
- connection, color = connection
- else:
- color = INFOCOL
-
# truncate very long connection description
- if len(connection) > 80:
- connection = f"{connection[0:80]}..."
+ if len(msg) > 80:
+ msg = f"{msg[0:80]}..."
if hasattr(self, "frm_status"):
- self.conn_label.after(
- 0, self.conn_label.config, {"text": connection, "fg": color}
- )
+ self.frm_status.lbl_connection["text"] = msg
+ self.frm_status.lbl_connection["fg"] = col
+ self.frm_status.lbl_connection.update()
self.update_idletasks()
- @property
- def device_label(self) -> Label:
+ def set_device_label(self, msg: str, col: str = INFOCOL):
"""
- Getter for device_label.
+ Sets device description in status bar.
- :return: status label
- :rtype: Label
+ :param str msg: connection descriptor
+ :param str col: color
"""
- return self.frm_status.lbl_device
-
- @device_label.setter
- def device_label(self, device: str | tuple[str, str]):
- """
- Sets device description in status bar (if known).
-
- :param str | tuple connection: (connection, color)
- """
-
- if isinstance(device, tuple):
- device, color = device
- else:
- color = INFOCOL
-
- # truncate long device description
- if len(device) > 30:
- device = f"{device[0:30]}..."
+ # truncate very long device description
+ if len(msg) > 30:
+ msg = f"{msg[0:30]}..."
if hasattr(self, "frm_status"):
- self.device_label.after(
- 0,
- self.device_label.config,
- {"text": device, "fg": color},
- )
+ self.frm_status.lbl_device["text"] = msg
+ self.frm_status.lbl_device["fg"] = col
+ self.frm_status.lbl_device.update()
self.update_idletasks()
# update configuration panels
@@ -1153,49 +996,41 @@ def device_label(self, device: str | tuple[str, str]):
if hasattr(self.dialog(dlg), "frm_device_info"):
self.dialog(dlg).frm_device_info.reset()
- @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]):
+ def set_status_label(self, msg: str = "", col: str = INFOCOL, expire: bool = True):
"""
Sets status message, or defers if frm_status not yet instantiated.
- :param str | tuple message: (message, color)
+ :param str msg: status message
+ :param str col: color
+ :param bool expire: expire messages
"""
def priority(col):
return STATUS_PRIORITY.get(col, 0)
- if isinstance(message, tuple):
- message, color = message
- else:
- color = INFOCOL
+ if self.status_msg_timer is not None:
+ self.after_cancel(self.status_msg_timer)
# truncate very long messages
- if len(message) > 200:
- message = "..." + message[-200:]
+ if len(msg) > 200:
+ msg = "..." + msg[-200:]
if hasattr(self, "frm_status"):
- color = INFOCOL if color == "blue" else color
- self.status_label.config(text=message, fg=color)
- self.status_label.update()
- self._last_status_update = datetime.now()
+ col = INFOCOL if col == "blue" else col
+ self.frm_status.lbl_status["text"] = msg
+ self.frm_status.lbl_status["fg"] = col
+ self.frm_status.lbl_status.update()
+ 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)
+ if priority(col) > defpty:
+ self._deferredmsg = (msg, col)
+
+ if expire:
+ self.status_msg_timer = self.after(STATUS_TIMEOUT, self.set_status_label)
@property
def conn_status(self) -> int:
@@ -1220,8 +1055,11 @@ def conn_status(self, status: int):
self.frm_banner.update_conn_status(status)
self.frm_settings.frm_settings.enable_controls(status)
if status == DISCONNECTED:
- self.conn_label = (NOTCONN, INFOCOL)
- self.device_label = NA
+ self.set_conn_label(NOTCONN, INFOCOL)
+ self.set_device_label(NA)
+ self.stream_handler.stop()
+ else:
+ self.refresh_widgets()
@property
def server_status(self) -> int:
@@ -1350,10 +1188,10 @@ def do_app_update(self, *args, **kwargs) -> int: # pylint: disable=unused-argum
"""
if brew_installed():
- self.status_label = (BREWUPDATE, INFOCOL)
+ self.set_status_label(BREWUPDATE, INFOCOL)
return 0
- self.status_label = (UPDATEINPROG, INFOCOL)
+ self.set_status_label(UPDATEINPROG, INFOCOL)
updates = [
nam for (nam, current, latest) in check_for_updates() if latest != current
]
@@ -1382,10 +1220,23 @@ def do_app_update(self, *args, **kwargs) -> int: # pylint: disable=unused-argum
try:
self.logger.debug(f"{executable=} {pth=} {cmd=}")
result = run(cmd, check=True, capture_output=True)
- self.status_label = (UPDATERESTART, OKCOL)
+ self.set_status_label(UPDATERESTART, OKCOL)
self.logger.debug(result.stdout)
return 1
except CalledProcessError as err:
- self.status_label = (UPDATEERR.format(err=err), ERRCOL)
+ self.set_status_label(UPDATEERR.format(err=err), ERRCOL)
self.logger.error(result.stdout)
return 0
+
+ def memory_monitor(self):
+ """
+ FOR MEMORY DEBUG.
+ """
+
+ snapshot = tracemalloc.take_snapshot()
+ top_stats = snapshot.statistics("lineno")
+
+ for stat in top_stats[:10]:
+ self.logger.info(stat)
+
+ self.after(60000, self.memory_monitor)
diff --git a/src/pygpsclient/attitude_frame.py b/src/pygpsclient/attitude_frame.py
index 4e15a00a..484ac215 100644
--- a/src/pygpsclient/attitude_frame.py
+++ b/src/pygpsclient/attitude_frame.py
@@ -24,6 +24,7 @@
Spinbox,
StringVar,
TclError,
+ Tk,
W,
)
@@ -73,17 +74,16 @@ class AttitudeFrame(Frame):
Attitude (3D orientation) frame class.
"""
- def __init__(self, app: Frame, parent: Frame, *args, **kwargs):
+ def __init__(self, app: Tk, parent: Frame, *args, **kwargs):
"""
Constructor.
- :param Frame app: reference to main tkinter application
+ :param Tk app: reference to main tkinter application
:param Frame parent: reference to parent frame
:param args: Optional args to pass to Frame parent class
:param kwargs: Optional kwargs to pass to Frame parent class
"""
self.__app = app
- self.__master = self.__app.appmaster
super().__init__(parent, *args, **kwargs)
@@ -236,7 +236,7 @@ def init_frame(self):
def update_frame(self):
"""
- Collect scatterplot data and update the plot.
+ Collect attitude data and update the plot.
"""
self._canvas.delete(DATA)
width, _ = self.get_size()
@@ -350,9 +350,10 @@ def update_frame(self):
tag=DATA,
)
+ self.update_idletasks()
+
except (KeyError, ValueError):
self._canvas.delete(DATA)
- self._canvas.update_idletasks()
def _flag_range(self, over: bool = False):
"""
diff --git a/src/pygpsclient/banner_frame.py b/src/pygpsclient/banner_frame.py
index 5ef9d07b..762229c9 100644
--- a/src/pygpsclient/banner_frame.py
+++ b/src/pygpsclient/banner_frame.py
@@ -12,7 +12,8 @@
:license: BSD 3-Clause
"""
-from tkinter import NE, NW, SUNKEN, Button, Frame, Label, N, W
+from os import getenv, getpid
+from tkinter import NE, NW, SUNKEN, Button, Frame, Label, N, Tk, W
from PIL import Image, ImageTk
from pynmeagps.nmeahelpers import latlon2dmm, latlon2dms, llh2ecef
@@ -58,6 +59,15 @@
)
from pygpsclient.strings import DGPSYES, NA
+PSSTATS = False
+try:
+ from psutil import Process
+
+ if int(getenv("PYGPSCLIENT_PSSTATS", "0")) & 1:
+ PSSTATS = True
+except ModuleNotFoundError:
+ pass
+
M2MILES = 5280
FONTBASE = 12
FONTSCALE = 90
@@ -68,18 +78,16 @@ class BannerFrame(Frame):
Banner frame class.
"""
- def __init__(self, app: Frame, parent: Frame, *args, **kwargs):
+ def __init__(self, app: Tk, *args, **kwargs):
"""
Constructor.
- :param Frame app: reference to main tkinter application
- :param Frame parent: reference to parent frame
+ :param Tk app: reference to main tkinter application
:param args: optional args to pass to Frame parent class
:param kwargs: optional kwargs to pass to Frame parent class
"""
self.__app = app # Reference to main application class
- self.__master = self.__app.appmaster # Reference to root class (Tk)
self._status = False
self._show_advanced = False
@@ -96,8 +104,9 @@ def __init__(self, app: Frame, parent: Frame, *args, **kwargs):
self._img_spartn = ImageTk.PhotoImage(Image.open(ICON_SPARTNCONFIG))
self._img_blank = ImageTk.PhotoImage(Image.open(ICON_BLANK))
self._sep = False
+ self._old_vms = 0
- super().__init__(parent, *args, **kwargs)
+ super().__init__(app, *args, **kwargs)
self.width, self.height = self.get_size()
@@ -199,6 +208,7 @@ def _body(self):
self._frm_basic, bg=BGCOL, fg="orange", width=17, anchor=W
)
self._lbl_fix = Label(self._frm_basic, bg=BGCOL, fg="white", width=10, anchor=W)
+ self._lbl_psstats = Label(self._frm_basic, bg=BGCOL, fg="yellow", anchor=W)
self._lbl_hmsl = Label(
self._frm_advanced, bg=BGCOL, fg="orange", width=13, anchor=W
)
@@ -263,6 +273,7 @@ def _do_layout(self):
self._lbl_lon.grid(column=6, row=0, pady=0, padx=0, sticky=W)
self._lbl_lfix.grid(column=7, row=0, pady=0, padx=0, sticky=W)
self._lbl_fix.grid(column=8, row=0, pady=0, padx=0, sticky=W)
+ self._lbl_psstats.grid(column=9, row=0, pady=0, padx=0, sticky=W)
self._lbl_lhmsl.grid(column=0, row=0, pady=0, padx=0, sticky=W)
self._lbl_hmsl.grid(column=1, row=0, pady=0, padx=0, sticky=W)
@@ -313,11 +324,11 @@ def _toggle_advanced(self):
if self._show_advanced:
self._frm_advanced.grid(column=1, row=1, pady=2, sticky=W)
self._frm_advanced2.grid(column=1, row=2, pady=2, sticky=W)
- self._btn_toggle.config(image=self._img_contract)
+ self._btn_toggle["image"] = self._img_contract
else:
self._frm_advanced.grid_forget()
self._frm_advanced2.grid_forget()
- self._btn_toggle.config(image=self._img_expand)
+ self._btn_toggle["image"] = self._img_expand
def update_conn_status(self, status: int):
"""
@@ -361,13 +372,16 @@ def update_transmit_status(self, transmit: int = 1):
if transmit > 0:
self._lbl_transmit_preset.configure(image=self._img_transmit)
- self._lbl_clients.config(text=transmit, fg="#6b8839")
+ self._lbl_clients["text"] = transmit
+ self._lbl_clients["fg"] = "#6b8839"
elif transmit == 0:
self._lbl_transmit_preset.configure(image=self._img_noclient)
- self._lbl_clients.config(text=transmit, fg="#e7b03e")
+ self._lbl_clients["text"] = transmit
+ self._lbl_clients["fg"] = "#e7b03e"
else:
self._lbl_transmit_preset.configure(image=self._img_blank)
- self._lbl_clients.config(text=" ", fg=BGCOL)
+ self._lbl_clients["text"] = " "
+ self._lbl_clients["fg"] = BGCOL
def update_frame(self):
"""
@@ -384,17 +398,15 @@ def update_frame(self):
self._update_siv()
self._update_dop(units)
self._update_dgps(units)
+ self._update_psstats()
+ self.update_idletasks()
def _update_time(self):
"""
Update GNSS time of week
"""
- tim = self.__app.gnss_status.utc
- if tim in (None, ""):
- self._lbl_time.config(text=NA)
- else:
- self._lbl_time.config(text=f"{tim:%H:%M:%S.%f}")
+ self._lbl_time["text"] = str(self.__app.gnss_status.utc)
def _update_pos(self, pos_format, units):
"""
@@ -408,11 +420,11 @@ def _update_pos(self, pos_format, units):
lon = self.__app.gnss_status.lon
alt = self.__app.gnss_status.alt # hMSL
hae = self.__app.gnss_status.hae
- self._lbl_llat.config(text="lat:")
- self._lbl_llon.config(text="lon:")
- self._lbl_lhmsl.config(text="hmsl:")
+ self._lbl_llat["text"] = "lat:"
+ self._lbl_llon["text"] = "lon:"
+ self._lbl_lhmsl["text"] = "hmsl:"
lhae = "sep:" if self._sep else "hae:"
- self._lbl_lhae.config(text=lhae)
+ self._lbl_lhae["text"] = lhae
alt_u = "ft" if units in (UI, UIK) else "m"
try:
@@ -420,11 +432,11 @@ def _update_pos(self, pos_format, units):
lat, lon, alt = llh2ecef(lat, lon, alt)
if units in (UI, UIK):
lat, lon = (m2ft(x) for x in (lat, lon))
- self._lbl_llat.config(text="X:")
- self._lbl_llon.config(text="Y:")
- self._lbl_lhmsl.config(text="Z:")
- self._lbl_lat.config(text=f"{lat:.4f}")
- self._lbl_lon.config(text=f"{lon:.4f}")
+ self._lbl_llat["text"] = "X:"
+ self._lbl_llon["text"] = "Y:"
+ self._lbl_lhmsl["text"] = "Z:"
+ self._lbl_lat["text"] = f"{lat:.4f}"
+ self._lbl_lon["text"] = f"{lon:.4f}"
else:
deg_f = "<15"
if pos_format == DMS:
@@ -436,16 +448,16 @@ def _update_pos(self, pos_format, units):
if units in (UI, UIK):
alt = m2ft(alt)
hae = m2ft(hae)
- self._lbl_lat.config(text=f"{lat:{deg_f}}")
- self._lbl_lon.config(text=f"{lon:{deg_f}}")
- self._lbl_hmsl.config(text=f"{alt:.4f} {alt_u}")
+ self._lbl_lat["text"] = f"{lat:{deg_f}}"
+ self._lbl_lon["text"] = f"{lon:{deg_f}}"
+ self._lbl_hmsl["text"] = f"{alt:.4f} {alt_u}"
haev = hae - alt if self._sep else hae
- self._lbl_hae.config(text=f"{haev:.4f} {alt_u}")
+ self._lbl_hae["text"] = f"{haev:.4f} {alt_u}"
except (TypeError, ValueError):
- self._lbl_lat.config(text=NA)
- self._lbl_lon.config(text=NA)
- self._lbl_hmsl.config(text=NA)
- self._lbl_hae.config(text=NA)
+ self._lbl_lat["text"] = NA
+ self._lbl_lon["text"] = NA
+ self._lbl_hmsl["text"] = NA
+ self._lbl_hae["text"] = NA
def _update_track(self, units):
"""
@@ -466,14 +478,14 @@ def _update_track(self, units):
elif units == UMK:
speed = ms2kmph(speed)
speed_u = "kmph"
- self._lbl_spd.config(text=f"{speed:.2f} {speed_u}")
+ self._lbl_spd["text"] = f"{speed:.2f} {speed_u}"
else:
- self._lbl_spd.config(text=NA)
+ self._lbl_spd["text"] = NA
track = self.__app.gnss_status.track
if isinstance(track, (int, float)):
- self._lbl_trk.config(text=f"{track:05.1f} °")
+ self._lbl_trk["text"] = f"{track:05.1f} °"
else:
- self._lbl_trk.config(text=NA)
+ self._lbl_trk["text"] = NA
def _update_fix(self):
"""
@@ -482,12 +494,12 @@ def _update_fix(self):
fix = self.__app.gnss_status.fix
if fix in ("3D", "GNSS+DR", "RTK", "RTK FIXED", "RTK FLOAT"):
- self._lbl_fix.config(fg="green2")
+ self._lbl_fix["fg"] = "green2"
elif fix in ("2D", "DR"):
- self._lbl_fix.config(fg="orange")
+ self._lbl_fix["fg"] = "orange"
else:
- self._lbl_fix.config(fg=ERRCOL)
- self._lbl_fix.config(text=fix)
+ self._lbl_fix["fg"] = ERRCOL
+ self._lbl_fix["text"] = fix
def _update_siv(self):
"""
@@ -503,11 +515,11 @@ def _update_siv(self):
else siv - unused_sats(self.__app.gnss_status.gsv_data)
)
try:
- self._lbl_siv.config(text=f"{siv:02d}")
- self._lbl_sip.config(text=f"{self.__app.gnss_status.sip:02d}")
+ self._lbl_siv["text"] = f"{siv:02d}"
+ self._lbl_sip["text"] = f"{self.__app.gnss_status.sip:02d}"
except (TypeError, ValueError):
- self._lbl_siv.config(text=NA)
- self._lbl_sip.config(text=NA)
+ self._lbl_siv["text"] = NA
+ self._lbl_sip["text"] = NA
def _update_dop(self, units):
"""
@@ -518,33 +530,33 @@ def _update_dop(self, units):
try:
pdop = self.__app.gnss_status.pdop
- self._lbl_pdop.config(text=f"{pdop:.2f} {dop2str(pdop)}")
+ self._lbl_pdop["text"] = f"{pdop:.2f} {dop2str(pdop)}"
except (TypeError, ValueError):
- self._lbl_pdop.config(text=NA)
+ self._lbl_pdop["text"] = NA
try:
- self._lbl_hvdop.config(
- text=f"hdop {self.__app.gnss_status.hdop:.2f}\n"
+ self._lbl_hvdop["text"] = (
+ f"hdop {self.__app.gnss_status.hdop:.2f}\n"
+ f"vdop {self.__app.gnss_status.vdop:.2f}"
)
except (TypeError, ValueError):
- self._lbl_hvdop.config(text=f"hdop {NA}\nvdop {NA}")
+ self._lbl_hvdop["text"] = f"hdop {NA}\nvdop {NA}"
try:
if units in (UI, UIK):
- self._lbl_hvacc.config(
- text=f"hacc {m2ft(self.__app.gnss_status.hacc):.3f}\n"
+ self._lbl_hvacc["text"] = (
+ f"hacc {m2ft(self.__app.gnss_status.hacc):.3f}\n"
+ f"vacc {m2ft(self.__app.gnss_status.vacc):.3f}"
)
- self._lbl_lacc_u.config(text="ft")
+ self._lbl_lacc_u["text"] = "ft"
else:
- self._lbl_hvacc.config(
- text=f"hacc {self.__app.gnss_status.hacc:.3f}\n"
+ self._lbl_hvacc["text"] = (
+ f"hacc {self.__app.gnss_status.hacc:.3f}\n"
+ f"vacc {self.__app.gnss_status.vacc:.3f}"
)
- self._lbl_lacc_u.config(text="m")
+ self._lbl_lacc_u["text"] = "m"
except (TypeError, ValueError):
- self._lbl_hvacc.config(text=f"hacc {NA}\nvacc {NA}")
+ self._lbl_hvacc["text"] = f"hacc {NA}\nvacc {NA}"
def _update_dgps(self, units):
"""
@@ -553,9 +565,7 @@ def _update_dgps(self, units):
:param str units: distance units as string (UMM, UMK, UI, UIK)
"""
- self._lbl_diffcorr.config(
- text=DGPSYES if self.__app.gnss_status.diff_corr else NA
- )
+ self._lbl_diffcorr["text"] = DGPSYES if self.__app.gnss_status.diff_corr else NA
baseline = self.__app.gnss_status.rel_pos_length
bl = NA
bl_u = ""
@@ -586,13 +596,27 @@ def _update_dgps(self, units):
if station in [None, "", 0]:
station = NA
if bl == NA:
- self._lbl_diffstat.config(text=f"age {age}\nid {station} - {bl}")
+ self._lbl_diffstat["text"] = f"age {age}\nid {station} - {bl}"
else:
- self._lbl_diffstat.config(
- text=f"age {age}\nid {station} - {bl:.2f} {bl_u}"
+ self._lbl_diffstat["text"] = (
+ f"age {age}\nid {station} - {bl:.2f} {bl_u}"
)
else:
- self._lbl_diffstat.config(text="")
+ self._lbl_diffstat["text"] = ""
+
+ def _update_psstats(self):
+ """
+ Update process statistics.
+ """
+
+ if PSSTATS:
+ pid = getpid()
+ process = Process(pid)
+ vms = int(process.memory_info().vms / 1024)
+ if self._old_vms == 0:
+ self._old_vms = vms
+ deltavms = vms - self._old_vms
+ self._lbl_psstats["text"] = f"pid: {pid}, vmsᵢ: {vms:,}, Δvms: {deltavms:,}"
def _set_fontsize(self):
"""
@@ -614,7 +638,7 @@ def _set_fontsize(self):
self._lbl_siv,
self._lbl_diffcorr,
):
- ctl.config(font=scale_font(self.width, FONTBASE, FONTSCALE)[0])
+ ctl["font"] = scale_font(self.width, FONTBASE, FONTSCALE)[0]
for ctl in (
self._lbl_ltime,
@@ -630,15 +654,16 @@ def _set_fontsize(self):
self._lbl_lsiv,
self._lbl_lacc,
self._lbl_lcorr,
+ self._lbl_psstats,
):
- ctl.config(font=scale_font(self.width, FONTBASE - 2, FONTSCALE)[0])
+ ctl["font"] = scale_font(self.width, FONTBASE - 2, FONTSCALE)[0]
for ctl in (
self._lbl_hvdop,
self._lbl_hvacc,
self._lbl_diffstat,
):
- ctl.config(font=scale_font(self.width, FONTBASE - 4, FONTSCALE)[0])
+ ctl["font"] = scale_font(self.width, FONTBASE - 4, FONTSCALE)[0]
def _on_sep(self, event): # pylint: disable=unused-argument
"""
diff --git a/src/pygpsclient/canvas_map.py b/src/pygpsclient/canvas_map.py
index 1efac313..69c7a51a 100644
--- a/src/pygpsclient/canvas_map.py
+++ b/src/pygpsclient/canvas_map.py
@@ -31,7 +31,9 @@
ALL,
CENTER,
Canvas,
+ Frame,
S,
+ Tk,
)
from PIL import Image, ImageTk, UnidentifiedImageError
@@ -106,18 +108,17 @@ class CanvasMap(Canvas): # pylint: disable=too-many-ancestors
Canvas Map class.
"""
- def __init__(self, app, container, *args, **kwargs):
+ def __init__(self, app: Tk, container: Frame, *args, **kwargs):
"""
Constructor.
- :param Frame app: reference to main tkinter application
+ :param Tk app: reference to main tkinter application
:param Frame container: reference to container frame
:param args: optional args to pass to Frame parent class
:param kwargs: optional kwargs to pass to Frame parent class
"""
self.__app = app # Reference to main application class
- self.__master = self.__app.appmaster # Reference to root class (Tk)
self.__container = container # container frame
self.width = kwargs.get("width", 400)
self.height = kwargs.get("height", 400)
@@ -141,7 +142,7 @@ def __init__(self, app, container, *args, **kwargs):
super().__init__(self.__container, *args, **kwargs)
- self.config(background=BGCOL)
+ self["background"] = BGCOL
self.bind("", self._on_resize)
self.bind("", self.on_clear)
self.bind("", self.on_clear)
diff --git a/src/pygpsclient/canvas_subclasses.py b/src/pygpsclient/canvas_subclasses.py
index 4d743bdc..6094558c 100644
--- a/src/pygpsclient/canvas_subclasses.py
+++ b/src/pygpsclient/canvas_subclasses.py
@@ -35,6 +35,7 @@
N,
S,
Scrollbar,
+ Tk,
W,
font,
)
@@ -119,7 +120,6 @@ def __init__(self, app, parent, *args, **kwargs):
"""
self.__app = app # Reference to main application class
- self.__master = self.__app.appmaster # Reference to root class (Tk)
self.x_scrollbar = Scrollbar(parent, orient=HORIZONTAL)
self.y_scrollbar = Scrollbar(parent, orient=VERTICAL)
@@ -134,8 +134,8 @@ def __init__(self, app, parent, *args, **kwargs):
self.frm_container = Frame(self, borderwidth=2, relief="groove")
self.grid(column=0, row=0, sticky=NSEW)
self.show_scroll()
- self.x_scrollbar.config(command=self.xview)
- self.y_scrollbar.config(command=self.yview)
+ self.x_scrollbar["command"] = self.xview
+ self.y_scrollbar["command"] = self.yview
# ensure container canvas expands to accommodate child frames
self.create_window((0, 0), window=self.frm_container, anchor=NW)
self.bind("", lambda e: self.config(scrollregion=self.bbox(ALL)))
@@ -171,7 +171,6 @@ def __init__(self, app, parent, *args, **kwargs):
"""
self.__app = app # Reference to main application class
- self.__master = self.__app.appmaster # Reference to root class (Tk)
super().__init__(parent, *args, **kwargs)
def create_graph(
@@ -499,7 +498,7 @@ class CanvasCompass(Canvas):
def __init__(
self,
- app: object,
+ app: Tk,
parent: Frame,
mode: Literal["ele", "lin"],
*args,
@@ -508,8 +507,8 @@ def __init__(
"""
Constructor.
- :param app: Application
- :param parent: Parent frame
+ :param Tk app: main Tk Application
+ :param Frame parent: Parent frame
:param int mode: ele (celestial) or lin (polar) coordinate system
"""
@@ -519,7 +518,6 @@ def __init__(
)
self.__app = app # Reference to main application class
- self.__master = self.__app.appmaster # Reference to root class (Tk)
self._mode = mode
super().__init__(parent, *args, **kwargs)
diff --git a/src/pygpsclient/chart_frame.py b/src/pygpsclient/chart_frame.py
index c005b983..5755f68f 100644
--- a/src/pygpsclient/chart_frame.py
+++ b/src/pygpsclient/chart_frame.py
@@ -26,6 +26,7 @@
Spinbox,
StringVar,
TclError,
+ Tk,
font,
)
@@ -100,18 +101,17 @@ class ChartviewFrame(Frame):
CHartview frame class.
"""
- def __init__(self, app, parent, *args, **kwargs):
+ def __init__(self, app: Tk, parent: Frame, *args, **kwargs):
"""
Constructor.
- :param Frame app: reference to main tkinter application
+ :param Tk app: reference to main tkinter application
:param Frame parent: reference to parent frame
:param args: optional args to pass to Frame parent class
:param kwargs: optional kwargs to pass to Frame parent class
"""
self.__app = app # Reference to main application class
- self.__master = self.__app.appmaster # Reference to root class (Tk)
super().__init__(parent, *args, **kwargs)
@@ -499,6 +499,7 @@ def update_frame(self):
"""
self._update_plot(self._chart_data)
+ self.update_idletasks()
def init_frame(self):
"""
@@ -622,10 +623,10 @@ def _on_clipboard(self, event): # pylint: disable=unused-argument
except KeyError:
csv += ","
csv += "\n"
- self.__master.clipboard_clear()
- self.__master.clipboard_append(csv)
- self.__master.update()
- self.__app.status_label = (CONTENTCOPIED.format("chart"), INFOCOL)
+ self.__app.clipboard_clear()
+ self.__app.clipboard_append(csv)
+ self.__app.update()
+ self.__app.set_status_label(CONTENTCOPIED.format("chart"), INFOCOL)
def _on_resize(self, event): # pylint: disable=unused-argument
"""
diff --git a/src/pygpsclient/configuration.py b/src/pygpsclient/configuration.py
index 2bfad4b3..9e0f1278 100644
--- a/src/pygpsclient/configuration.py
+++ b/src/pygpsclient/configuration.py
@@ -92,12 +92,11 @@ def __init__(self, app):
"""
Set up initial configuration.
- :param Frame app: reference to main tkinter application
+ :param Tk app: reference to main tkinter application
:param str filename: configuration file name
"""
self.__app = app # Reference to main application class
- self.__master = self.__app.appmaster # Reference to root class (Tk)
self.logger = logging.getLogger(__name__)
# Set initial default configuration
@@ -122,6 +121,7 @@ def __init__(self, app):
"spartnprot_b": 0,
"mqttprot_b": 1,
"ttyprot_b": 0,
+ "metaprot_b": 0,
"ttycrlf_b": 1,
"ttyecho_b": 0,
"ttydelay_b": 1,
@@ -247,6 +247,9 @@ def __init__(self, app):
"lbandclientunqword_s": D9S_PP["unqword"],
"lbandclientoutport_s": PASSTHRU,
"lbandclientdebug_b": 0,
+ "relposnedsettings_d": {
+ "source_s": "NAV-RELPOSNED",
+ },
"scattersettings_d": {
"scatterautorange_b": 1,
"scattercenter_s": "Average",
@@ -307,9 +310,9 @@ def loadfile(self, filename: str | NoneType = None) -> tuple:
if err == "": # config valid
rs = LOADCONFIGRESAVE if resave else ""
- self.__app.status_label = (LOADCONFIGOK.format(fname, rs), OKCOL)
+ self.__app.set_status_label(LOADCONFIGOK.format(fname, rs), OKCOL)
else:
- self.__app.status_label = (err, ERRCOL)
+ self.__app.set_status_label(err, ERRCOL)
return fname, err
diff --git a/src/pygpsclient/confirm_box.py b/src/pygpsclient/confirm_box.py
index fe6364e4..f0e82ec4 100644
--- a/src/pygpsclient/confirm_box.py
+++ b/src/pygpsclient/confirm_box.py
@@ -34,8 +34,8 @@ def __init__(self, parent, title, prompt):
:param string prompt: prompt to be displayed
"""
- self.__master = parent
- Toplevel.__init__(self, parent)
+ self.__parent = parent
+ super().__init__(parent)
self.title(title) # pylint: disable=E1102
self.attributes("-topmost", True) # keep on top
self.resizable(False, False)
@@ -78,10 +78,10 @@ def _centre(self):
# self.update_idletasks()
dw = self.winfo_width()
dh = self.winfo_height()
- mx = self.__master.winfo_x()
- my = self.__master.winfo_y()
- mw = self.__master.winfo_width()
- mh = self.__master.winfo_height()
+ mx = self.__parent.winfo_x()
+ my = self.__parent.winfo_y()
+ mw = self.__parent.winfo_width()
+ mh = self.__parent.winfo_height()
self.geometry(f"+{int(mx + (mw/2 - dw/2))}+{int(my + (mh/2 - dh/2))}")
def show(self):
diff --git a/src/pygpsclient/console_frame.py b/src/pygpsclient/console_frame.py
index 135e90d5..1d4538bf 100644
--- a/src/pygpsclient/console_frame.py
+++ b/src/pygpsclient/console_frame.py
@@ -17,6 +17,7 @@
:license: BSD 3-Clause
"""
+from queue import Empty
from tkinter import (
END,
EW,
@@ -28,10 +29,9 @@
Frame,
Scrollbar,
Text,
+ Tk,
)
-from pyubx2 import hextable
-
from pygpsclient.globals import (
BGCOL,
DISCONNECTED,
@@ -43,9 +43,11 @@
FORMAT_BOTH,
FORMAT_HEXSTR,
FORMAT_HEXTAB,
+ FORMAT_PARSED,
INFOCOL,
WIDGETU3,
)
+from pygpsclient.helpers import hextable
from pygpsclient.strings import CONTENTCOPIED, HALTTAGWARN
HALT = "HALT"
@@ -57,18 +59,17 @@ class ConsoleFrame(Frame):
Console frame class.
"""
- def __init__(self, app: Frame, parent: Frame, *args, **kwargs):
+ def __init__(self, app: Tk, parent: Frame, *args, **kwargs):
"""
Constructor.
- :param Frame app: reference to main tkinter application
+ :param Tk app: reference to main tkinter application
:param Frame parent: reference to parent frame
:param args: optional args to pass to Frame parent class
:param kwargs: optional kwargs to pass to Frame parent class
"""
self.__app = app # Reference to main application class
- self.__master = self.__app.appmaster # Reference to root class (Tk)
super().__init__(parent, *args, **kwargs)
@@ -104,8 +105,8 @@ def _body(self):
wrap=NONE,
height=15,
)
- self.sblogh.config(command=self.txt_console.xview)
- self.sblogv.config(command=self.txt_console.yview)
+ self.sblogh["command"] = self.txt_console.xview
+ self.sblogv["command"] = self.txt_console.yview
# making the textbox read only and fixed width font
self.txt_console.configure(state="disabled")
@@ -137,13 +138,16 @@ def _attach_events(self):
self.txt_console.bind("", self._on_clipboard)
# self.txt_console.tag_bind(HALT, "<1>", self._on_halt) # doesn't seem to work on MacOS
- def update_frame(self, consoledata: list):
+ def update_frame(self):
"""
Print the formatted data stream to the console.
'maxlines' defines the maximum number of scrollable lines that are
retained in the text box on a FIFO basis.
+ To minimise refresh latency, only the last 'maxlines' of queued
+ messages are written to the console; the remainder are discarded.
+
:param list consoledata: list of tuples (raw, parsed, marker) \
accumulated since last console update
"""
@@ -152,32 +156,49 @@ def update_frame(self, consoledata: list):
return
consoleformat = self.__app.configuration.get("consoleformat_s")
- colortagging = self.__app.configuration.get("colortag_b")
maxlines = self.__app.configuration.get("maxlines_n")
self._halt = ""
- consolestr = ""
- self.txt_console.configure(font=FONT_TEXT)
- for raw_data, parsed_data, marker in consoledata:
- if consoleformat == FORMAT_BINARY:
- data = f"{marker}{raw_data}\n"
- elif consoleformat == FORMAT_HEXSTR:
- data = f"{marker}{raw_data.hex()}\n"
- elif consoleformat == FORMAT_HEXTAB:
- self.txt_console.configure(font=FONT_FIXED)
- data = hextable(raw_data)
- elif consoleformat == FORMAT_BOTH:
- self.txt_console.configure(font=FONT_FIXED)
- data = f"{marker}{parsed_data}\n{hextable(raw_data)}"
- else:
- data = f"{marker}{parsed_data}\n"
-
- consolestr += data
-
+ self.txt_console["font"] = (
+ FONT_TEXT if consoleformat in (FORMAT_BINARY, FORMAT_PARSED) else FONT_FIXED
+ )
+ raw_data = None
+ parsed_data = None
+ lines = []
+ while True:
+ try:
+ raw_data, parsed_data, marker = self.__app.console_outqueue.get(False)
+ if self.__app.console_outqueue.qsize() < maxlines:
+ if consoleformat == FORMAT_BINARY:
+ lines.append(f"{marker}{raw_data}\n")
+ elif consoleformat == FORMAT_HEXSTR:
+ lines.append(f"{marker}{raw_data.hex()}\n")
+ elif consoleformat == FORMAT_HEXTAB:
+ lines += hextable(raw_data)
+ elif consoleformat == FORMAT_BOTH:
+ lines.append(f"{marker}{parsed_data}\n")
+ lines += hextable(raw_data)
+ else:
+ lines.append(f"{marker}{parsed_data}\n")
+ self.__app.console_outqueue.task_done()
+ except Empty:
+ break
+
+ consolestr = "".join(lines[-maxlines:])
numlinesbefore = self.numlines
self.txt_console.configure(state="normal")
- self.txt_console.insert(END, consolestr)
- if colortagging:
+ if len(lines) >= maxlines:
+ self.txt_console.delete("1.0", "end")
+ self.txt_console.insert("1.0", consolestr)
+ numlinesbefore = 0
+ else:
+ excess = self.numlines + len(lines) - maxlines
+ if excess > 0:
+ self.txt_console.delete("1.0", f"{excess}.0")
+ numlinesbefore -= excess
+ self.txt_console.insert(END, consolestr)
+
+ if self.__app.configuration.get("colortag_b"):
self._tag_line(self.txt_console, numlinesbefore, self.numlines)
if self._halt != "":
self._on_halt(None)
@@ -187,7 +208,7 @@ def update_frame(self, consoledata: list):
self.txt_console.see("end")
self.txt_console.configure(state="disabled")
- self.txt_console.update_idletasks()
+ self.update_idletasks()
def _tag_line(self, con, startline: int, endline: int):
"""
@@ -231,7 +252,7 @@ def _on_halt(self, event): # pylint: disable=unused-argument
"""
self.__app.stream_handler.stop()
- self.__app.status_label = (HALTTAGWARN.format(self._halt), ERRCOL)
+ self.__app.set_status_label(HALTTAGWARN.format(self._halt), ERRCOL)
self.__app.conn_status = DISCONNECTED
def _on_clipboard(self, event): # pylint: disable=unused-argument
@@ -241,10 +262,10 @@ def _on_clipboard(self, event): # pylint: disable=unused-argument
:param event event: double click event
"""
- self.__master.clipboard_clear()
- self.__master.clipboard_append(self.txt_console.get("1.0", END))
- self.__master.update()
- self.__app.status_label = (CONTENTCOPIED.format("console"), INFOCOL)
+ self.__app.clipboard_clear()
+ self.__app.clipboard_append(self.txt_console.get("1.0", END))
+ self.__app.update()
+ self.__app.set_status_label(CONTENTCOPIED.format("console"), INFOCOL)
def _on_resize(self, event): # pylint: disable=unused-argument
"""
diff --git a/src/pygpsclient/dialog_state.py b/src/pygpsclient/dialog_state.py
index c59e21bf..2a872d14 100644
--- a/src/pygpsclient/dialog_state.py
+++ b/src/pygpsclient/dialog_state.py
@@ -40,9 +40,11 @@
DLGTSPARTN,
DLGTTTY,
DLGTUBX,
+ DLGTUBXLEGACY,
)
from pygpsclient.tty_preset_dialog import TTYPresetDialog
from pygpsclient.ubx_config_dialog import UBXConfigDialog
+from pygpsclient.ubx_legacy_config_dialog import UBXLegacyConfigDialog
class DialogState:
@@ -64,6 +66,11 @@ def __init__(self):
DLGTUBX: {
CLASS: UBXConfigDialog,
DLG: None,
+ RESIZE: True,
+ },
+ DLGTUBXLEGACY: {
+ CLASS: UBXLegacyConfigDialog,
+ DLG: None,
RESIZE: False,
},
DLGTNMEA: {
diff --git a/src/pygpsclient/dynamic_config_frame.py b/src/pygpsclient/dynamic_config_frame.py
index fcc54698..69d59428 100644
--- a/src/pygpsclient/dynamic_config_frame.py
+++ b/src/pygpsclient/dynamic_config_frame.py
@@ -38,6 +38,7 @@
S,
Scrollbar,
StringVar,
+ Tk,
W,
)
@@ -74,7 +75,7 @@
# dimensions of scrollable attribute window
SCROLLX = 370
-SCROLLY = 300
+SCROLLY = 200
NMEA = "NMEA"
UBX = "UBX"
ACK = "ACK-ACK"
@@ -152,18 +153,17 @@ class Dynamic_Config_Frame(Frame):
Dynamic configuration command panel.
"""
- def __init__(self, app: Frame, parent: Frame, *args, **kwargs):
+ def __init__(self, app: Tk, parent: Frame, *args, **kwargs):
"""
Constructor.
- :param Frame app: reference to main tkinter application
+ :param Tk app: reference to main tkinter application
:param Frame parent: reference to parent frame (config-dialog)
:param args: optional args to pass to Frame parent class
:param kwargs: optional kwargs to pass to Frame parent class
"""
self.__app = app # Reference to main application class
- self.__master = self.__app.appmaster # Reference to root class (Tk)
self.__container = parent
self.logger = logging.getLogger(__name__)
self._protocol = kwargs.pop("protocol", "UBX")
@@ -200,14 +200,13 @@ def _body(self):
self._scr_cfg_cmd = Scrollbar(
self, orient=VERTICAL, command=self._lbx_cfg_cmd.yview
)
- self._lbx_cfg_cmd.config(yscrollcommand=self._scr_cfg_cmd.set)
+ self._lbx_cfg_cmd["yscrollcommand"] = self._scr_cfg_cmd.set
self._lbl_send_command = Label(self, image=self.__container.img_none)
self._btn_send_command = Button(
self,
image=self.__container.img_send,
width=50,
command=self._on_set_cfg,
- font=self.__app.font_md,
cursor=CLICK_CURSOR,
)
self._btn_refresh = Button(
@@ -215,7 +214,6 @@ def _body(self):
image=self.__container.img_redraw,
width=40,
command=self._on_refresh,
- font=self.__app.font_md,
cursor=CLICK_CURSOR,
)
self._lbl_command = Label(self, text="", anchor=W)
@@ -228,10 +226,8 @@ def _body(self):
self._scr_container_hor = Scrollbar(
self._frm_container, orient="horizontal", command=self._can_container.xview
)
- self._can_container.config(
- yscrollcommand=self._scr_container_ver.set,
- xscrollcommand=self._scr_container_hor.set,
- )
+ self._can_container["yscrollcommand"] = self._scr_container_ver.set
+ self._can_container["xscrollcommand"] = self._scr_container_hor.set
self._can_container.create_window(0, 0, window=self._frm_attrs, anchor=NW)
def _do_layout(self):
@@ -240,7 +236,7 @@ def _do_layout(self):
"""
self._lbl_cfg_dyn.grid(column=0, row=0, columnspan=3, sticky=EW)
- self._lbx_cfg_cmd.grid(column=0, row=1, columnspan=1, sticky=EW)
+ self._lbx_cfg_cmd.grid(column=0, row=1, columnspan=1, sticky=NSEW)
self._scr_cfg_cmd.grid(column=1, row=1, sticky=(N, S, W))
self._btn_send_command.grid(column=2, row=1, ipadx=3, ipady=3, sticky=NE)
self._lbl_send_command.grid(column=3, row=1, ipadx=3, ipady=3, sticky=NE)
@@ -262,7 +258,7 @@ def _do_layout(self):
)
self._scr_container_ver.grid(column=5, row=0, sticky=(N, S, W))
self._scr_container_hor.grid(column=0, row=2, columnspan=5, sticky=EW)
- self.option_add("*Font", self.__app.font_sm)
+ self.grid_rowconfigure(1, weight=1)
def _attach_events(self):
"""
@@ -288,7 +284,7 @@ def reset(self):
self._lbx_cfg_cmd.insert(i, cmd)
self._clear_widgets()
- self._lbl_send_command.config(image=self.__container.img_unknown)
+ self._lbl_send_command["image"] = self.__container.img_unknown
def _setscroll(self, event): # pylint: disable=unused-argument
"""
@@ -327,7 +323,7 @@ def _on_select_cfg(self, *args, **kwargs): # pylint: disable=unused-argument
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}")
+ self._lbl_command["text"] = f"{pdesc}"
self._clear_widgets()
self._add_widgets(pdic, 1, 0)
self.update()
@@ -370,7 +366,7 @@ 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.__container.img_pending)
+ self._lbl_send_command["image"] = self.__container.img_pending
self.__container.status_label = f"P{self._cfg_id} SET message sent"
for msgid in pendcfg:
self.__container.set_pending(msgid, penddlg)
@@ -419,13 +415,13 @@ def _do_poll_cfg(self, *args, **kwargs): # pylint: disable=unused-argument
if msg is not None:
self.__container.send_command(msg)
self.__container.status_label = f"{cp}{cfg_id} POLL message sent"
- self._lbl_send_command.config(image=self.__container.img_pending)
+ self._lbl_send_command["image"] = self.__container.img_pending
for msgid in pendcfg:
self.__container.set_pending(msgid, penddlg)
self._expected_response = POLL
else: # CFG cannot be POLLed
self.__container.status_label = f"{cp}{cfg_id} No POLL available"
- self._lbl_send_command.config(image=self.__container.img_unknown)
+ self._lbl_send_command["image"] = self.__container.img_unknown
def _do_poll_args(self, cfg_id: str) -> dict:
"""
@@ -507,10 +503,10 @@ def update_status(self, msg: NMEAMessage | UBXMessage):
f"{cfg_id} message acknowledged",
OKCOL,
)
- self._lbl_send_command.config(image=self.__container.img_confirmed)
+ self._lbl_send_command["image"] = self.__container.img_confirmed
else:
self.__container.status_label = (f"{cfg_id} message rejected", ERRCOL)
- self._lbl_send_command.config(image=self.__container.img_warn)
+ self._lbl_send_command["image"] = self.__container.img_warn
self.update()
def _clear_widgets(self):
@@ -548,22 +544,23 @@ def _add_widgets(self, pdict: dict, row: int, index: int) -> int:
for nam, att in pdict.items(): # process each attribute in dict
if isinstance(att, tuple): # repeating group or bitfield
numr, attd = att
- if numr in (
- X1,
- X2,
- X4,
- X6,
- X8,
- X24,
- ): # bitfield
- row = self._add_widgets(attd, row, index)
- else: # repeating group
- if isinstance(numr, int): # fixed length group
- nr = numr
- else:
- nr = 1
- for idx in range(nr):
- row = self._add_widgets_group(att, row, idx + 1)
+ if isinstance(attd, dict):
+ if numr in (
+ X1,
+ X2,
+ X4,
+ X6,
+ X8,
+ X24,
+ ): # bitfield
+ row = self._add_widgets(attd, row, index)
+ else: # repeating group
+ if isinstance(numr, int): # fixed length group
+ nr = numr
+ else:
+ nr = 1
+ for idx in range(nr):
+ row = self._add_widgets_group(att, row, idx + 1)
else: # single attribute
row = self._add_widgets_single(nam, att, row, index)
diff --git a/src/pygpsclient/file_handler.py b/src/pygpsclient/file_handler.py
index 975848e5..62f6d40e 100644
--- a/src/pygpsclient/file_handler.py
+++ b/src/pygpsclient/file_handler.py
@@ -44,6 +44,7 @@
from pygpsclient.strings import CONFIGTITLE, GITHUB_URL, SAVETITLE
DEFEXT = ("all files", "*.*")
+FLUSHINT = 100 # flush log file every 100 updates
class FileHandler:
@@ -55,11 +56,10 @@ def __init__(self, app):
"""
Constructor.
- :param Frame app: reference to main tkinter application
+ :param Tk app: reference to main tkinter application
"""
self.__app = app # Reference to main application class
- self.__master = self.__app.appmaster # Reference to root class (Tk)
self.logger = logging.getLogger(__name__)
self._in_filepath = None
self._in_filename = None
@@ -75,6 +75,7 @@ def __init__(self, app):
self._initdir = {}
self._logsize = 0
self._last_track_update = datetime.fromordinal(1)
+ self._flushcount = 0
def __del__(self):
"""
@@ -111,12 +112,12 @@ def open_file(
self._initdir[mode] = Path(fil).parent # remember last directory
return fil
- def load_config(self, filename: Path = CONFIGFILE) -> tuple:
+ def load_config(self, filename: Path | str = CONFIGFILE) -> tuple:
"""
Load configuration file. If filename is not provided, defaults
to $HOME/pygpsclient.json, otherwise user is prompted for path.
- :param Path filename: fully qualified filename, or None for prompt
+ :param Path | str filename: fully qualified filename, or None for prompt
:return: filename, saved settings as dictionary and any error message
:rtype: tuple
"""
@@ -183,13 +184,13 @@ def validate_config(self, config: dict) -> str:
break
return err
- def save_config(self, config: dict, filename: Path = CONFIGFILE) -> str:
+ def save_config(self, config: dict, filename: Path | str = CONFIGFILE) -> str:
"""
Save configuration file. If filename is not provided, defaults to
$HOME/pygpsclient.json, otherwise user is prompted for filename.
:param dict config: configuration settings as dictionary
- :param Path filename: fully qualified path to config file, or None for prompt
+ :param Path | str filename: fully qualified path to config file, or None for prompt
:return: return code "" = success, err str = failure
:rtype: str
"""
@@ -215,7 +216,7 @@ 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 | NoneType:
+ def set_logfile_path(self, initdir=HOME) -> Path | str | NoneType:
"""
Set file path.
@@ -248,7 +249,7 @@ def open_logfile(self) -> int:
self._logfile = open(self._logname, "a+b")
return 1
except FileNotFoundError as err:
- self.__app.status_label = (f"{err}", ERRCOL)
+ self.__app.set_status_label(f"{err}", ERRCOL)
return 0
def write_logfile(self, raw_data, parsed_data):
@@ -279,7 +280,10 @@ def write_logfile(self, raw_data, parsed_data):
datum = (str(datum) + "\r").encode("utf-8")
try:
self._logfile.write(datum)
- self._logfile.flush()
+ self._flushcount += 1
+ if self._flushcount >= FLUSHINT:
+ self._logfile.flush()
+ self._flushcount = 0
self._logsize += len(datum)
except ValueError:
pass
@@ -330,7 +334,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.status_label = (f"{err}", ERRCOL)
+ self.__app.set_status_label(f"{err}", ERRCOL)
return 0
date = datetime.now().isoformat() + "Z"
diff --git a/src/pygpsclient/globals.py b/src/pygpsclient/globals.py
index cb29ca6a..a9ed6ae1 100644
--- a/src/pygpsclient/globals.py
+++ b/src/pygpsclient/globals.py
@@ -56,6 +56,7 @@
SPARTN_PROTOCOL = 256
MQTT_PROTOCOL = 512
TTY_PROTOCOL = 1024
+GNSS_PROTOCOL = 2048
# Various global constants - please keep in ascending alphabetical order
HOME = Path.home()
@@ -184,6 +185,9 @@
ICON_TRANSMIT = path.join(DIRNAME, "resources/iconmonstr-transmit-10-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_UBXCONFIGLEGACY = path.join(
+ DIRNAME, "resources/iconmonstr-gear-2-24-ubxlegacy.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")
@@ -247,8 +251,9 @@
SPARTN_SOURCE_LB = 1
SPONSOR_URL = "https://buymeacoffee.com/semuconsulting"
SQRT2 = 0.7071067811865476 # square root of 2
+STREAMDELAY = 0.001 # sleep period between stream reads, in seconds
STATUS_PRIORITY = {INFOCOL: 0, "blue": 0, OKCOL: 2, "green": 1, ERRCOL: 3, "red": 3}
-STATUS_TIMEOUT = 15 # timeout for stale status messages in seconds
+STATUS_TIMEOUT = 15000 # timeout for stale status messages in milliseconds
TIME0 = datetime(1970, 1, 1) # basedate for time()
TIMEOUTS = ("0.1", "0.2", "0.5", "1", "2", "5", "10", "20", "None", "0")
# map nmea talker to gnss_id
@@ -373,7 +378,7 @@
"GLLD": "3D",
"GLLE": "DR",
"GNSA": "3D", # posMode
- "GNSD": "3D",
+ "GNSD": "RTK",
"GNSF": "RTK FLOAT",
"GNSR": "RTK FIXED",
"GNSE": "DR",
@@ -383,7 +388,7 @@
"RMCR": "RTK FIXED",
"RMCE": "DR",
"VTGA": "3D", # posMode
- "VTGD": "RTK",
+ "VTGD": "3D",
"VTGE": "DR",
"NAV-PVT1": "DR", # fixType
"NAV-PVT2": "2D",
diff --git a/src/pygpsclient/gnss_status.py b/src/pygpsclient/gnss_status.py
index a3bd43d9..98d5daca 100644
--- a/src/pygpsclient/gnss_status.py
+++ b/src/pygpsclient/gnss_status.py
@@ -57,6 +57,7 @@ def reset(self):
self.base_ecefx = 0.0 # base station ECEF X
self.base_ecefy = 0.0 # base station ECEF Y
self.base_ecefz = 0.0 # base station ECEF Z
+ self.rel_pos_source = "" # relative position data source
self.rel_pos_heading = 0.0 # rover relative position heading
self.rel_pos_length = 0.0 # rover relative position distance
self.acc_heading = 0.0 # rover relative position heading accuracy
diff --git a/src/pygpsclient/gpx_dialog.py b/src/pygpsclient/gpx_dialog.py
index 9d66c088..8d0169ef 100644
--- a/src/pygpsclient/gpx_dialog.py
+++ b/src/pygpsclient/gpx_dialog.py
@@ -26,6 +26,7 @@
N,
Spinbox,
StringVar,
+ Tk,
W,
)
from xml.dom import minidom
@@ -84,12 +85,11 @@
class GPXViewerDialog(ToplevelDialog):
"""GPXViewerDialog class."""
- def __init__(self, app, *args, **kwargs):
+ def __init__(self, app: Tk, *args, **kwargs):
"""Constructor."""
self.__app = app
self.logger = logging.getLogger(__name__)
- # self.__master = self.__app.appmaster # link to root Tk window
super().__init__(app, DLGTGPX)
self._mapzoom = IntVar()
self._maptype = StringVar()
@@ -338,10 +338,11 @@ def _on_redraw(self, *args, **kwargs):
Handle redraw button press.
"""
- self.status_label = (DLGGPXWAIT, INFOCOL)
+ self.set_status_label(DLGGPXWAIT, INFOCOL)
self._detach_events()
self._reset()
- self._spn_zoom.config(highlightbackground="gray90", highlightthickness=3)
+ self._spn_zoom["highlightbackground"] = "gray90"
+ self._spn_zoom["highlightthickness"] = 3
self._parse_gpx()
self._draw_map()
self._draw_profile()
@@ -367,7 +368,7 @@ def _on_load(self):
self._gpxfile = self._open_gpxfile()
if self._gpxfile is None: # user cancelled
return
- self.status_label = (DLGGPXLOAD, INFOCOL)
+ self.set_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.status_label = (f"{DLGGPXERROR}\n{repr(err)}", ERRCOL)
+ self.set_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.status_label = (DLGGPXNULL.format(ptyp), ERRCOL)
+ self.set_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.status_label = (DLGGPXLOADED, INFOCOL)
+ self.set_status_label(DLGGPXLOADED, INFOCOL)
def _draw_map(self):
"""
@@ -484,9 +485,11 @@ def _draw_map(self):
)
if self._can_mapview.zoommin:
- self._spn_zoom.config(highlightbackground=ERRCOL, highlightthickness=3)
+ self._spn_zoom["highlightbackground"] = ERRCOL
+ self._spn_zoom["highlightthickness"] = 3
else:
- self._spn_zoom.config(highlightbackground="gray90", highlightthickness=3)
+ self._spn_zoom["highlightbackground"] = "gray90"
+ self._spn_zoom["highlightthickness"] = 3
def _draw_profile(self):
"""
diff --git a/src/pygpsclient/hardware_info_frame.py b/src/pygpsclient/hardware_info_frame.py
index 07cfeff3..1cad4af4 100644
--- a/src/pygpsclient/hardware_info_frame.py
+++ b/src/pygpsclient/hardware_info_frame.py
@@ -10,7 +10,7 @@
:license: BSD 3-Clause
"""
-from tkinter import EW, Frame, Label, W
+from tkinter import EW, Frame, Label, Tk, W
from PIL import Image, ImageTk
@@ -30,11 +30,11 @@ class Hardware_Info_Frame(Frame):
Hardware & firmware information panel.
"""
- def __init__(self, app: Frame, parent: Frame, *args, **kwargs):
+ def __init__(self, app: Tk, parent: Frame, *args, **kwargs):
"""
Constructor.
- :param Frame app: reference to main tkinter application
+ :param Tk app: reference to main tkinter application
:param Frame parent: reference to parent frame (config-dialog)
:param args: optional args to pass to Frame parent class
:param kwargs: optional kwargs to pass to Frame parent class
@@ -112,18 +112,17 @@ def reset(self, event=None): # pylint: disable=unused-argument):):
Reset panel to initial settings
"""
- self._lbl_swver.config(
- text=self.__app.gnss_status.version_data.get("swversion", NA)
+ self._lbl_swver["text"] = self.__app.gnss_status.version_data.get(
+ "swversion", NA
)
- self._lbl_hwver.config(
- text=self.__app.gnss_status.version_data.get("hwversion", NA)
+ self._lbl_hwver["text"] = self.__app.gnss_status.version_data.get(
+ "hwversion", NA
)
- self._lbl_fwver.config(
- text=self.__app.gnss_status.version_data.get("fwversion", NA)
+ self._lbl_fwver["text"] = self.__app.gnss_status.version_data.get(
+ "fwversion", NA
)
- self._lbl_romver.config(
- text=self.__app.gnss_status.version_data.get("romversion", NA)
+ self._lbl_romver["text"] = self.__app.gnss_status.version_data.get(
+ "romversion", NA
)
- self._lbl_gnss.config(text=self.__app.gnss_status.version_data.get("gnss", NA))
-
+ self._lbl_gnss["text"] = self.__app.gnss_status.version_data.get("gnss", NA)
self.__container.status_label = (HWRESET, OKCOL)
diff --git a/src/pygpsclient/helpers.py b/src/pygpsclient/helpers.py
index 1c35ff92..cc0e6bf6 100644
--- a/src/pygpsclient/helpers.py
+++ b/src/pygpsclient/helpers.py
@@ -684,6 +684,34 @@ def get_units(units: str) -> tuple:
return dst_u, dst_c, ele_u, ele_c, spd_u, spd_c
+def hextable(raw: bytes, cols: int = 8) -> list:
+ """
+ Formats raw (binary) message in tabular hexadecimal format e.g.
+
+ 000: 2447 4e47 5341 2c41 2c33 2c33 342c 3233 | b'$GNGSA,A,3,34,23' |
+
+ :param bytes raw: raw (binary) data
+ :param int cols: number of columns in hex table (8)
+ :return: table of hex data
+ :rtype: list
+ """
+
+ lines = []
+ if raw is None:
+ return lines
+ colw = cols * 4
+ rawh = raw.hex()
+ for i in range(0, len(rawh), colw):
+ rawl = rawh[i : i + colw].ljust(colw, " ")
+ hextbl = f"{int(i/2):03}: "
+ for col in range(0, colw, 4):
+ hextbl += f"{rawl[col : col + 4]} "
+ bfh = str(bytes.fromhex(rawl))
+ hextbl += f" | {bfh:<67} |\n"
+ lines.append(hextbl)
+ return lines
+
+
def hdg2yaw(heading: float) -> float:
"""
Convert heading (0 - 360) to yaw (-180 - 180)
diff --git a/src/pygpsclient/importmap_dialog.py b/src/pygpsclient/importmap_dialog.py
index 3186e7f2..e57e27d9 100644
--- a/src/pygpsclient/importmap_dialog.py
+++ b/src/pygpsclient/importmap_dialog.py
@@ -26,6 +26,7 @@
IntVar,
Label,
StringVar,
+ Tk,
W,
)
@@ -57,11 +58,11 @@
class ImportMapDialog(ToplevelDialog):
"""ImportMapDialog class."""
- def __init__(self, app, *args, **kwargs):
+ def __init__(self, app: Tk, *args, **kwargs):
"""Constructor."""
self.__app = app
- # self.__master = self.__app.appmaster # link to root Tk window
+
super().__init__(app, DLGTIMPORTMAP)
self.width = int(kwargs.get("width", 400))
self.height = int(kwargs.get("height", 400))
@@ -175,9 +176,9 @@ def _reset(self):
self._first.set(0)
self._can_mapview.delete(ALL)
- # self._btn_import.config(state=DISABLED)
+ # self._btn_import["state"]=DISABLED
if not HASRASTERIO:
- self.status_label = (
+ self.set_status_label(
"Rasterio library is not installed - "
"extents must be entered manually",
INFOCOL,
@@ -199,11 +200,11 @@ def _valid_entries(self) -> bool:
valid = valid & self._ent_minlon.validate(VALFLOAT, -180, 180)
valid = valid & self._ent_maxlon.validate(VALFLOAT, -180, 180)
if valid:
- self.status_label = ""
- self._btn_import.config(state=NORMAL)
+ self.set_status_label("")
+ self._btn_import["state"] = NORMAL
else:
- self.status_label = ("Error: invalid entry", ERRCOL)
- self._btn_import.config(state=DISABLED)
+ self.set_status_label("Error: invalid entry", ERRCOL)
+ self._btn_import["state"] = DISABLED
return valid
def _on_load(self):
@@ -211,16 +212,16 @@ def _on_load(self):
Load custom map from file.
"""
- self.status_label = ""
+ self.set_status_label("")
self._custommap = self._open_mapfile()
if self._custommap is not None:
bounds = self._get_bounds(self._custommap)
self._can_mapview.draw_map(
maptype=IMPORT, mappath=self._custommap, bounds=bounds, zoom=None
)
- self._btn_import.config(state=NORMAL)
+ self._btn_import["state"] = NORMAL
else:
- self._btn_import.config(state=DISABLED)
+ self._btn_import["state"] = DISABLED
def _on_redraw(self, *args, **kwargs):
"""
@@ -266,14 +267,14 @@ def _get_bounds(self, mappath) -> Area:
extents = transform_bounds(ras.crs.to_epsg(), 4326, *ras.bounds)
lonmin, latmin, lonmax, latmax = extents
except AttributeError:
- self.status_label = (
+ self.set_status_label(
"Image is not georeferenced - extents must be entered manually",
ERRCOL,
)
except RasterioIOError:
- self.status_label = ("Image is not a supported file format", ERRCOL)
+ self.set_status_label("Image is not a supported file format", ERRCOL)
except Exception as err: # pylint: disable=broad-exception-caught
- self.status_label = (str(err), ERRCOL)
+ self.set_status_label(str(err), ERRCOL)
self._lonmin.set(str(round(lonmin, 8)))
self._latmin.set(str(round(latmin, 8)))
@@ -300,10 +301,10 @@ def _on_import(self):
latmax = float(self._latmax.get())
if lonmax + 180 <= lonmin + 180 or latmax + 90 <= latmin + 90:
- self.status_label = ("Error: minimum must be less than maximum", ERRCOL)
+ self.set_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.status_label = ("Custom map imported", OKCOL)
+ self.set_status_label("Custom map imported", OKCOL)
diff --git a/src/pygpsclient/init_presets.py b/src/pygpsclient/init_presets.py
index e2a0b026..1395d9eb 100644
--- a/src/pygpsclient/init_presets.py
+++ b/src/pygpsclient/init_presets.py
@@ -134,6 +134,9 @@
"Unicore UM98n Disable basic NMEA on current port; unlog gpgsa; unlog gpgga; unlog gpgll; unlog gpgsv; unlog gpvtg; unlog gprmc",
"Unicore UM98n Disable basic UNI on current port; unlog PVTSLNB; unlog SATSINFOB; unlog BESTNAVB; unlog STADOPB",
"Unicore UM98n Enable RAW Observations on current port (NB may need to increase baudrate or timeout!); OBSVMB 1",
+ "Unicore UM98n Enable Ephemerides on current port; GPSEPHB ONCHANGED; GALEPHB ONCHANGED; GLOEPHB ONCHANGED; BDSEPHB ONCHANGED; BD3EPHB ONCHANGED; QZSSEPHB ONCHANGED; IRNSSEPHB ONCHANGED",
+ "Unicore UM98n Enable Time Corrections on current port; GPSUTCB ONCHANGED; GALUTCB ONCHANGED; BDSUTCB ONCHANGED; BD3UTCB ONCHANGED",
+ "Unicore UM98n Enable Ionospheric Corrections on current port; GPSIONB ONCHANGED; GALIONB ONCHANGED; BDSIONB ONCHANGED; BD3IONB ONCHANGED",
"Unicore UM98n Query Version; version",
"Unicore UM98n Query Config; config",
"Unicore UM98n Stop All Output on COM1; unlog COM1",
diff --git a/src/pygpsclient/levelsview_frame.py b/src/pygpsclient/levelsview_frame.py
index acd81b6f..6ef63d60 100644
--- a/src/pygpsclient/levelsview_frame.py
+++ b/src/pygpsclient/levelsview_frame.py
@@ -14,7 +14,7 @@
# pylint: disable=no-member
-from tkinter import NE, NSEW, Frame, font
+from tkinter import NE, NSEW, Frame, Tk, font
from pygpsclient.canvas_subclasses import (
TAG_DATA,
@@ -46,18 +46,17 @@ class LevelsviewFrame(Frame):
Levelsview frame class.
"""
- def __init__(self, app: Frame, parent: Frame, *args, **kwargs):
+ def __init__(self, app: Tk, parent: Frame, *args, **kwargs):
"""
Constructor.
- :param Frame app: reference to main tkinter application
+ :param Tk app: reference to main tkinter application
:param Frame parent: reference to parent frame
:param args: optional args to pass to Frame parent class
:param kwargs: optional kwargs to pass to Frame parent class
"""
self.__app = app # Reference to main application class
- self.__master = self.__app.appmaster # Reference to root class (Tk)
super().__init__(parent, *args, **kwargs)
@@ -88,8 +87,6 @@ def _attach_events(self):
self.bind("", self._on_resize)
self._canvas.bind("", self._on_legend)
- self._canvas.bind("", self._on_cno0)
- self._canvas.bind("", self._on_cno0)
def _on_legend(self, event): # pylint: disable=unused-argument
"""
@@ -103,18 +100,6 @@ def _on_legend(self, event): # pylint: disable=unused-argument
)
self._redraw = True
- def _on_cno0(self, event): # pylint: disable=unused-argument
- """
- On double-right-click - include levels where C/No = 0.
-
- :param event: event
- """
-
- self.__app.configuration.set(
- "unusedsat_b", not self.__app.configuration.get("unusedsat_b")
- )
- self._redraw = True
-
def init_frame(self):
"""
Initialise graph view
@@ -173,8 +158,8 @@ def update_frame(self):
Automatically adjust y axis according to number of satellites in view.
"""
- data = self.__app.gnss_status.gsv_data
show_unused = self.__app.configuration.get("unusedsat_b")
+ data = self.__app.gnss_status.gsv_data
siv = len(data)
siv = siv if show_unused else siv - unused_sats(data)
if siv <= 0:
@@ -189,8 +174,6 @@ def update_frame(self):
xfnt, _, _, _ = fitfont(XLBLFMT, colwidth, self._canvas.yoffb, XLBLANGLE)
for val in sorted(data.values()): # sort by ascending gnssid, svid
gnssId, prn, _, _, cno, _ = val
- if cno == 0 and not show_unused:
- continue
snr_y = int(cno) * (h - self._canvas.yoffb - 1) / MAX_SNR
_, ol_col = GNSS_LIST[gnssId]
self._canvas.create_rectangle(
@@ -214,10 +197,10 @@ def update_frame(self):
tags=TAG_DATA,
)
offset += colwidth
+ self.update_idletasks()
if self.__app.configuration.get("legend_b"):
self._draw_legend()
- 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 0a664539..4af6c887 100644
--- a/src/pygpsclient/map_frame.py
+++ b/src/pygpsclient/map_frame.py
@@ -33,6 +33,7 @@
Label,
Spinbox,
StringVar,
+ Tk,
W,
)
@@ -67,18 +68,17 @@ class MapviewFrame(Frame):
Map frame class.
"""
- def __init__(self, app: Frame, parent: Frame, *args, **kwargs):
+ def __init__(self, app: Tk, parent: Frame, *args, **kwargs):
"""
Constructor.
- :param Frame app: reference to main tkinter application
+ :param Tk app: reference to main tkinter application
:param Frame parent: reference to parent frame
:param args: optional args to pass to Frame parent class
:param kwargs: optional kwargs to pass to Frame parent class
"""
self.__app = app # Reference to main application class
- self.__master = self.__app.appmaster # Reference to root class (Tk)
super().__init__(parent, *args, **kwargs)
@@ -220,11 +220,11 @@ def on_maptype(self, var, index, mode): # pylint: disable=unused-argument
return
if self._maptype.get() == WORLD:
- self._lbl_zoom.config(state=DISABLED)
- self._spn_zoom.config(state=DISABLED)
+ self._lbl_zoom["state"] = DISABLED
+ self._spn_zoom["state"] = DISABLED
else:
- self._lbl_zoom.config(state=NORMAL)
- self._spn_zoom.config(state=READONLY)
+ self._lbl_zoom["state"] = NORMAL
+ self._spn_zoom["state"] = READONLY
self.__app.configuration.set("maptype_s", self._maptype.get())
self._lastmaptype = self._maptype.get()
@@ -307,9 +307,13 @@ def update_frame(self):
self._bounds = self._canvas.bounds
if self._canvas.zoommin:
- self._spn_zoom.config(highlightbackground=ERRCOL, highlightthickness=3)
+ self._spn_zoom["highlightbackground"] = ERRCOL
+ self._spn_zoom["highlightthickness"] = 3
else:
- self._spn_zoom.config(highlightbackground="gray90", highlightthickness=3)
+ self._spn_zoom["highlightbackground"] = "gray90"
+ self._spn_zoom["highlightthickness"] = 3
+
+ self.update_idletasks()
def _on_resize(self, event): # pylint: disable=unused-argument
"""
diff --git a/src/pygpsclient/menu_bar.py b/src/pygpsclient/menu_bar.py
index 64549abf..c8b992ca 100644
--- a/src/pygpsclient/menu_bar.py
+++ b/src/pygpsclient/menu_bar.py
@@ -26,6 +26,7 @@
DLGTSERVER,
DLGTTTY,
DLGTUBX,
+ DLGTUBXLEGACY,
MENUABOUT,
MENUEXIT,
MENUFILE,
@@ -39,6 +40,7 @@
DIALOGS = (
DLGTUBX,
+ DLGTUBXLEGACY,
DLGTNMEA,
DLGTNTRIP,
DLGTSERVER,
@@ -60,14 +62,13 @@ def __init__(self, app, *args, **kwargs):
"""
Constructor
- :param Frame app: reference to main tkinter application
+ :param Tk app: reference to main tkinter application
:param args: optional args to pass to Menu parent class
:param kwargs: optional kwargs to pass to Menu parent class
"""
- self.__app = app # Reference to main application class
- self.__master = self.__app.appmaster # Reference to root class (Tk)
- Menu.__init__(self, self.__master, *args, **kwargs)
+ self.__app = app # Reference to main Tk application class
+ super().__init__(self.__app, *args, **kwargs)
self.option_add("*Font", "TkMenuFont")
# Create a pull-down menu for file operations
diff --git a/src/pygpsclient/nmea_config_dialog.py b/src/pygpsclient/nmea_config_dialog.py
index 513d1691..f78dc7de 100644
--- a/src/pygpsclient/nmea_config_dialog.py
+++ b/src/pygpsclient/nmea_config_dialog.py
@@ -44,7 +44,7 @@ def __init__(self, app, *args, **kwargs): # pylint: disable=unused-argument
"""
Constructor.
- :param Frame app: reference to main tkinter application
+ :param Tk app: reference to main tkinter application
:param args: optional args to pass to parent class (not currently used)
:param kwargs: optional kwargs to pass to parent class (not currently used)
"""
@@ -93,15 +93,12 @@ def _do_layout(self):
self.container.grid_columnconfigure(0, weight=1)
self.container.grid_columnconfigure(1, weight=1)
self.container.grid_rowconfigure(1, weight=1)
- colsp, rowsp = self._frm_preset.grid_size()
+ colsp, _ = self._frm_preset.grid_size()
for col in range(colsp - 2):
self._frm_preset.grid_columnconfigure(col, weight=1)
self._frm_preset.grid_rowconfigure(2, weight=1)
- colsp, rowsp = self._frm_config_dynamic.grid_size()
- for col in range(colsp):
- self._frm_config_dynamic.grid_columnconfigure(col, weight=1)
- for row in range(1, rowsp):
- self._frm_config_dynamic.grid_rowconfigure(row, weight=1)
+ self._frm_config_dynamic.grid_columnconfigure(0, weight=1)
+ self._frm_config_dynamic.grid_rowconfigure(1, weight=1)
def _reset(self):
"""
@@ -115,7 +112,7 @@ def _reset(self):
CONNECTED_SOCKET,
CONNECTED_SIMULATOR,
):
- self.status_label = ("Device not connected", ERRCOL)
+ self.set_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 d51fc4d9..4e1dab05 100644
--- a/src/pygpsclient/nmea_handler.py
+++ b/src/pygpsclient/nmea_handler.py
@@ -19,6 +19,7 @@
import logging
from math import degrees
from time import time
+from tkinter import Tk
from pynmeagps import NMEAMessage
from pyubx2 import itow2utc
@@ -34,15 +35,14 @@ class NMEAHandler:
NMEA handler class.
"""
- def __init__(self, app):
+ def __init__(self, app: Tk):
"""
Constructor.
- :param Frame app: reference to main tkinter application
+ :param Tk app: reference to main tkinter application
"""
self.__app = app # Reference to main application class
- self.__master = self.__app.appmaster # Reference to root class (Tk)
self.logger = logging.getLogger(__name__)
self._raw_data = None
@@ -65,9 +65,9 @@ def process_data(self, raw_data: bytes, parsed_data: NMEAMessage):
and "QTM" in parsed_data.msgID
):
self.__app.gnss_status.version_data["hwversion"] = "Quectel"
- self.__app.device_label = self.__app.gnss_status.version_data[
- "hwversion"
- ]
+ self.__app.set_device_label(
+ self.__app.gnss_status.version_data["hwversion"]
+ )
# self.logger.debug(f"data received {parsed_data.identity}")
if parsed_data.msgID in ("RMC", "RMCH"): # Recommended minimum data for GPS
self._process_RMC(parsed_data)
@@ -246,6 +246,7 @@ def _process_GSV(self, data: NMEAMessage):
:param pynmeagps.NMEAMessage data: parsed GSV sentence
"""
+ show_unused = self.__app.configuration.get("unusedsat_b")
gnss = TKGN.get(data.talker, 0)
now = time()
@@ -257,6 +258,8 @@ def _process_GSV(self, data: NMEAMessage):
cno = getattr(data, "cno" + idx, 0)
if not isinstance(cno, (int, float)):
cno = 0
+ if cno == 0 and not show_unused: # ignore sats with zero cno
+ continue
if svid != "":
svid = int(svid)
if gnss == 0 and (120 <= svid <= 158):
@@ -321,7 +324,9 @@ def _process_TXT(self, data: NMEAMessage):
hw_version = hw.replace("UBX-", "u-blox ")
break
self.__app.gnss_status.version_data["hwversion"] = hw_version
- self.__app.device_label = self.__app.gnss_status.version_data["hwversion"]
+ self.__app.set_device_label(
+ self.__app.gnss_status.version_data["hwversion"]
+ )
elif (
"ROM CORE " in data.text
and self.__app.gnss_status.version_data["fwversion"] == NA
@@ -375,6 +380,7 @@ def _process_UBX03(self, data: NMEAMessage):
"""
# pylint: disable=consider-using-dict-items
+ show_unused = self.__app.configuration.get("unusedsat_b")
self.__app.gnss_status.gsv_data = {}
now = time()
for i in range(data.numSv):
@@ -386,6 +392,8 @@ def _process_UBX03(self, data: NMEAMessage):
cno = getattr(data, "cno" + idx)
if not isinstance(cno, (int, float)):
cno = 0
+ if cno == 0 and not show_unused:
+ continue
# fudge to make PUBX03 svid numbering consistent with GSV
if gnss == 2 and svid > 210: # Galileo
svid -= 210
@@ -419,7 +427,7 @@ def _process_QTMVERNO(self, data: NMEAMessage):
if self.__app.dialog(DLGTNMEA) is not None:
self.__app.dialog(DLGTNMEA).update_pending(data)
- self.__app.device_label = self.__app.gnss_status.version_data["hwversion"]
+ self.__app.set_device_label(self.__app.gnss_status.version_data["hwversion"])
def _process_QTMVER(self, data: NMEAMessage):
"""
@@ -502,7 +510,7 @@ def _process_FMI(self, data: NMEAMessage):
pass
self.__app.gnss_status.version_data["hwversion"] = "Feyman IM19"
- self.__app.device_label = self.__app.gnss_status.version_data["hwversion"]
+ self.__app.set_device_label(self.__app.gnss_status.version_data["hwversion"])
def _process_IMU(self, data: NMEAMessage):
"""
diff --git a/src/pygpsclient/nmea_preset_frame.py b/src/pygpsclient/nmea_preset_frame.py
index 30b2d182..ae1f5707 100644
--- a/src/pygpsclient/nmea_preset_frame.py
+++ b/src/pygpsclient/nmea_preset_frame.py
@@ -28,6 +28,7 @@
S,
Scrollbar,
StringVar,
+ Tk,
W,
)
@@ -65,18 +66,17 @@ class NMEA_PRESET_Frame(Frame):
NMEA Preset and User-defined configuration command panel.
"""
- def __init__(self, app: Frame, parent: Frame, *args, **kwargs):
+ def __init__(self, app: Tk, parent: Frame, *args, **kwargs):
"""
Constructor.
- :param Frame app: reference to main tkinter application
+ :param Tk app: reference to main tkinter application
:param Frame parent: reference to parent frame (config-dialog)
:param args: optional args to pass to Frame parent class
:param kwargs: optional kwargs to pass to Frame parent class
"""
self.__app = app # Reference to main application class
- self.__master = self.__app.appmaster # Reference to root class (Tk)
self.logger = logging.getLogger(__name__)
self.__container = parent
@@ -122,10 +122,10 @@ def _body(self):
)
self._scr_presetv = Scrollbar(self, orient=VERTICAL)
self._scr_preseth = Scrollbar(self, orient=HORIZONTAL)
- self._lbx_preset.config(yscrollcommand=self._scr_presetv.set)
- self._lbx_preset.config(xscrollcommand=self._scr_preseth.set)
- self._scr_presetv.config(command=self._lbx_preset.yview)
- self._scr_preseth.config(command=self._lbx_preset.xview)
+ self._lbx_preset["yscrollcommand"] = self._scr_presetv.set
+ self._lbx_preset["xscrollcommand"] = self._scr_preseth.set
+ self._scr_presetv["command"] = self._lbx_preset.yview
+ self._scr_preseth["command"] = self._lbx_preset.xview
self._lbl_send_command = Label(self, image=self.__container.img_none)
self._btn_send_command = Button(
self,
@@ -140,9 +140,9 @@ def _do_layout(self):
Layout widgets.
"""
- self._lbl_command.grid(column=0, row=0, sticky=W)
- self._ent_command.grid(column=1, row=0, columnspan=4, sticky=EW)
- self._lbl_presets.grid(column=0, row=1, columnspan=5, sticky=EW)
+ self._lbl_presets.grid(column=0, row=0, columnspan=5, sticky=EW)
+ self._lbl_command.grid(column=0, row=1, sticky=W)
+ self._ent_command.grid(column=1, row=1, columnspan=4, sticky=EW)
self._lbx_preset.grid(column=0, row=2, columnspan=3, sticky=NSEW)
self._scr_presetv.grid(column=2, row=2, sticky=(N, S, E))
self._scr_preseth.grid(column=0, row=3, columnspan=3, sticky=EW)
@@ -152,7 +152,6 @@ def _do_layout(self):
self._lbl_send_command.grid(
column=4, row=2, padx=3, ipadx=3, ipady=3, sticky=NE
)
- self.option_add("*Font", self.__app.font_sm)
def _attach_events(self):
"""
@@ -204,7 +203,7 @@ def _on_send_preset(self, *args, **kwargs): # pylint: disable=unused-argument
status = CONFIRMED
if status == CONFIRMED:
- self._lbl_send_command.config(image=self._img_pending)
+ self._lbl_send_command["image"] = self._img_pending
self.__container.status_label = "Command(s) sent"
for msgid in confids:
self.__container.set_pending(msgid, NMEA_PRESET)
@@ -215,7 +214,7 @@ def _on_send_preset(self, *args, **kwargs): # pylint: disable=unused-argument
except Exception as err: # pylint: disable=broad-except
self.__container.status_label = (f"Error {err}", ERRCOL)
- self._lbl_send_command.config(image=self._img_warn)
+ self._lbl_send_command["image"] = self._img_warn
def _do_user_defined(self, command: str) -> list:
"""
@@ -248,7 +247,7 @@ def _do_user_defined(self, command: str) -> list:
self.__container.send_command(msg)
except Exception as err: # pylint: disable=broad-except
self.__container.status_label = (f"Error {err}", ERRCOL)
- self._lbl_send_command.config(image=self._img_warn)
+ self._lbl_send_command["image"] = self._img_warn
return confids
@@ -261,8 +260,8 @@ def update_status(self, msg: NMEAMessage):
status = getattr(msg, "status", "OK")
if status == "OK":
- self._lbl_send_command.config(image=self._img_confirmed)
+ self._lbl_send_command["image"] = self._img_confirmed
self.__container.status_label = ("Preset command(s) acknowledged", OKCOL)
elif status == "ERROR":
- self._lbl_send_command.config(image=self._img_warn)
+ self._lbl_send_command["image"] = self._img_warn
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 511db6c3..4149b7a3 100644
--- a/src/pygpsclient/ntrip_client_dialog.py
+++ b/src/pygpsclient/ntrip_client_dialog.py
@@ -42,6 +42,7 @@
Spinbox,
StringVar,
TclError,
+ Tk,
W,
ttk,
)
@@ -102,18 +103,17 @@ class NTRIPConfigDialog(ToplevelDialog):
NTRIPConfigDialog class.
"""
- def __init__(self, app, *args, **kwargs): # pylint: disable=unused-argument
+ def __init__(self, app: Tk, *args, **kwargs): # pylint: disable=unused-argument
"""
Constructor.
- :param Frame app: reference to main tkinter application
+ :param Tk app: reference to main tkinter application
:param args: optional args to pass to parent class (not currently used)
:param kwargs: optional kwargs to pass to parent class (not currently used)
"""
self.__app = app # Reference to main application class
self.logger = getLogger(__name__)
- self.__master = self.__app.appmaster # Reference to root class (Tk)
super().__init__(app, DLGTNTRIP)
self._cfg_msg_command = None
@@ -184,10 +184,10 @@ def _body(self):
)
self._scr_sourcetablev = Scrollbar(self._frm_body, orient=VERTICAL)
self._scr_sourcetableh = Scrollbar(self._frm_body, orient=HORIZONTAL)
- self._lbx_sourcetable.config(yscrollcommand=self._scr_sourcetablev.set)
- self._lbx_sourcetable.config(xscrollcommand=self._scr_sourcetableh.set)
- self._scr_sourcetablev.config(command=self._lbx_sourcetable.yview)
- self._scr_sourcetableh.config(command=self._lbx_sourcetable.xview)
+ self._lbx_sourcetable["yscrollcommand"] = self._scr_sourcetablev.set
+ self._lbx_sourcetable["xscrollcommand"] = self._scr_sourcetableh.set
+ self._scr_sourcetablev["command"] = self._lbx_sourcetable.yview
+ self._scr_sourcetableh["command"] = self._lbx_sourcetable.xview
self._lbl_ntripversion = Label(self._frm_body, text=LBLNTRIPVERSION)
self._spn_ntripversion = Spinbox(
@@ -435,21 +435,21 @@ def set_controls(self, connected: bool, msgt: NoneType | tuple = None):
else "Disconnected"
)
if msgt is None:
- self.status_label = (msg, INFOCOL)
+ self.set_status_label(msg, INFOCOL)
else:
msg, col = msgt
- self.status_label = (msg, col)
+ self.set_status_label(msg, col)
self._frm_socket.status_label = connected
- self._btn_disconnect.config(state=(NORMAL if connected else DISABLED))
+ self._btn_disconnect["state"] = NORMAL if connected else DISABLED
for ctl in (
self._spn_ntripversion,
self._spn_ntripggaint,
self._spn_datatype,
):
- ctl.config(state=(DISABLED if connected else READONLY))
+ ctl["state"] = DISABLED if connected else READONLY
for ctl in (
self._btn_connect,
@@ -464,7 +464,7 @@ def set_controls(self, connected: bool, msgt: NoneType | tuple = None):
self._rad_ggafixed,
self._lbx_sourcetable,
):
- ctl.config(state=(DISABLED if connected else NORMAL))
+ ctl["state"] = DISABLED if connected else NORMAL
# refresh sourcetable listbox ! NB PLACEMENT OF THIS CALL IS IMPORTANT !
self.update_sourcetable(self._settings["sourcetable"])
# update closest mountpoint name and distance (if available)
@@ -694,7 +694,7 @@ def _valid_settings(self) -> bool:
valid = valid & self._ent_sep.validate(VALFLOAT, -MAXALT, MAXALT)
if not valid:
- self.status_label = ("ERROR - invalid settings", ERRCOL)
+ self.set_status_label("ERROR - invalid settings", ERRCOL)
return valid
@@ -716,7 +716,7 @@ def set_mp_dist(self, dist: float, name: str = ""):
dist_u = "miles"
dist_l = f"Baseline: {dist:,.1f} {dist_u}"
self._ntrip_mountpoint.set(name)
- self._lbl_mpdist.config(text=dist_l)
+ self._lbl_mpdist["text"] = dist_l
def _get_coordinates(self) -> tuple[float, float]:
"""
diff --git a/src/pygpsclient/qgc_handler.py b/src/pygpsclient/qgc_handler.py
index 65f9875d..ad0750cd 100644
--- a/src/pygpsclient/qgc_handler.py
+++ b/src/pygpsclient/qgc_handler.py
@@ -29,11 +29,10 @@ def __init__(self, app):
"""
Constructor.
- :param Frame app: reference to main tkinter application
+ :param Tk app: reference to main tkinter application
"""
self.__app = app # Reference to main application class
- self.__master = self.__app.appmaster # Reference to root class (Tk)
self.logger = logging.getLogger(__name__)
self._raw_data = None
@@ -52,5 +51,7 @@ def process_data(self, raw_data: bytes, parsed_data: object):
pass
if self.__app.gnss_status.version_data["hwversion"] == NA:
self.__app.gnss_status.version_data["hwversion"] = "Quectel"
- self.__app.device_label = self.__app.gnss_status.version_data["hwversion"]
+ self.__app.set_device_label(
+ self.__app.gnss_status.version_data["hwversion"]
+ )
# self.logger.debug(f"data received {parsed_data.identity}")
diff --git a/src/pygpsclient/recorder_dialog.py b/src/pygpsclient/recorder_dialog.py
index 6ce3adf1..ce71b593 100644
--- a/src/pygpsclient/recorder_dialog.py
+++ b/src/pygpsclient/recorder_dialog.py
@@ -31,6 +31,7 @@
Label,
StringVar,
TclError,
+ Tk,
W,
filedialog,
)
@@ -93,18 +94,16 @@ class RecorderDialog(ToplevelDialog):
Configuration command recorder panel.
"""
- def __init__(self, app, *args, **kwargs):
+ def __init__(self, app: Tk, *args, **kwargs):
"""
Constructor.
- :param Frame app: reference to main tkinter application
- :param Frame container: reference to container frame (config-dialog)
+ :param Tk app: reference to main tkinter application
:param args: optional args to pass to Frame parent class
:param kwargs: optional kwargs to pass to Frame parent class
"""
self.__app = app
- # self.__master = self.__app.appmaster # link to root Tk window
super().__init__(app, DLGTRECORD)
self.width = int(kwargs.get("width", 500))
self.height = int(kwargs.get("height", 300))
@@ -307,7 +306,7 @@ def _on_load(self):
return
self.__app.recorded_commands = None
- self.status_label = ("Loading commands...", INFOCOL)
+ self.set_status_label("Loading commands...", INFOCOL)
try:
if self._configfile[-3:] == "txt":
@@ -318,12 +317,12 @@ def _on_load(self):
i = self._on_load_ubx(self._configfile)
except Exception: # pylint: disable=broad-exception-caught
i = 0
- self.status_label = (f"ERROR parsing {self._configfile}!", ERRCOL)
+ self.set_status_label(f"ERROR parsing {self._configfile}!", ERRCOL)
self.update_count()
if i > 0:
fname = self._configfile.split("/")[-1]
- self.status_label = (
+ self.set_status_label(
f"{i} Command{'s' if i > 1 else ''} loaded from {fname}",
OKCOL,
)
@@ -418,7 +417,7 @@ def _on_save(self):
return
if len(self.__app.recorded_commands) == 0:
- self.status_label = ("Nothing to save", ERRCOL)
+ self.set_status_label("Nothing to save", ERRCOL)
return
if self._save_to_preset: # saving to preset section of json config
@@ -430,7 +429,7 @@ def _on_save(self):
if self._configfile is None: # user cancelled
return
- self.status_label = ("Saving commands...", INFOCOL)
+ self.set_status_label("Saving commands...", INFOCOL)
i = 0
with open(self._configfile, "wb") as file:
for i, msg in enumerate(self.__app.recorded_commands):
@@ -439,7 +438,7 @@ def _on_save(self):
file.write(msg)
self.__app.recorded_commands = None
self.update_count()
- self.status_label = (
+ self.set_status_label(
f"{i + 1} command{'s' if i > 0 else ''} saved to {fname}",
OKCOL,
)
@@ -453,7 +452,7 @@ def _on_play(self):
return
if len(self.__app.recorded_commands) == 0:
- self.status_label = ("Nothing to send", ERRCOL)
+ self.set_status_label("Nothing to send", ERRCOL)
return
i = 0
@@ -461,13 +460,13 @@ def _on_play(self):
self._rec_status = PLAY
for i, msg in enumerate(self.__app.recorded_commands):
mid = getattr(self.__app.recorded_commands[-1], "identity", "tty")
- self.status_label = (f"{i} Sending {mid}", INFOCOL)
+ self.set_status_label(f"{i} Sending {mid}", INFOCOL)
if isinstance(msg, (UBXMessage, NMEAMessage)):
msg = msg.serialize()
self.__app.send_to_device(msg)
sleep(0.01)
self._rec_status = STOP
- self.status_label = (
+ self.set_status_label(
f"{i + 1} command{'s' if i > 0 else ''} sent to device",
OKCOL,
)
@@ -495,7 +494,7 @@ def _on_record(self):
self.__app.recording = False
stat = "started" if self._rec_status else "stopped"
- self.status_label = (f"Recording {stat}", INFOCOL)
+ self.set_status_label(f"Recording {stat}", INFOCOL)
self._update_status()
def _on_import(self):
@@ -507,14 +506,14 @@ def _on_import(self):
"""
if self._rec_status == RECORD:
- self.status_label = ("Stop recording first", ERRCOL)
+ self.set_status_label("Stop recording first", ERRCOL)
return
if len(self.__app.recorded_commands) == 0:
- self.status_label = ("Nothing to import", ERRCOL)
+ self.set_status_label("Nothing to import", ERRCOL)
return
- self.status_label = (
+ self.set_status_label(
f"Enter preset description, then click Save {SAVE}",
INFOCOL,
)
@@ -550,12 +549,12 @@ def _on_import_confirm(self):
)
typ = "TTY"
- self.status_label = (
+ self.set_status_label(
f"{len(self.__app.recorded_commands)} commands imported as {typ} presets",
OKCOL,
)
except AttributeError:
- self.status_label = (
+ self.set_status_label(
"Recorded commands must be of same type",
ERRCOL,
)
@@ -573,13 +572,13 @@ def _on_undo(self):
"""
if len(self.__app.recorded_commands) == 0:
- self.status_label = ("Nothing to undo", ERRCOL)
+ self.set_status_label("Nothing to undo", ERRCOL)
return
if self._rec_status == STOP:
if len(self.__app.recorded_commands) > 0:
self.__app.recorded_commands = UNDO
- self.status_label = ("Last command undone", INFOCOL)
+ self.set_status_label("Last command undone", INFOCOL)
self.update_count()
@@ -593,11 +592,11 @@ def _on_delete(self):
lcs = len(self.__app.recorded_commands)
if lcs == 0:
- self.status_label = ("Nothing to delete", ERRCOL)
+ self.set_status_label("Nothing to delete", ERRCOL)
return
self.__app.recorded_commands = None
- self.status_label = (f"{lcs} command{'s' if lcs > 1 else ''} deleted", INFOCOL)
+ self.set_status_label(f"{lcs} command{'s' if lcs > 1 else ''} deleted", INFOCOL)
self.update_count()
@@ -616,15 +615,15 @@ def _update_status(self):
elif self._rec_status == RECORD:
pimg = self._img_play
rimg = self._img_stop
- self._btn_play.config(image=pimg)
- self._btn_record.config(image=rimg)
+ self._btn_play["image"] = pimg
+ self._btn_record["image"] = rimg
def update_count(self):
"""
Update command count.
"""
- self._lbl_memory.config(text=len(self.__app.recorded_commands))
+ self._lbl_memory["text"] = len(self.__app.recorded_commands)
def _flash_record(self, stop: Event):
"""
@@ -637,11 +636,13 @@ def _flash_record(self, stop: Event):
i = 0
while not stop.is_set():
i = not i
- self._lbl_activity.config(
- text="RECORDING", fg=cols[i][0], bg=cols[i][1]
- )
+ self._lbl_activity["text"] = "RECORDING"
+ self._lbl_activity["fg"] = cols[i][0]
+ self._lbl_activity["bg"] = cols[i][1]
sleep(FLASH)
- self._lbl_activity.config(text="", fg=FGCOL, bg=BGCOL)
+ self._lbl_activity["text"] = ""
+ self._lbl_activity["fg"] = FGCOL
+ self._lbl_activity["bg"] = BGCOL
except TclError: # if dialog closed without stopping recording
pass
diff --git a/src/pygpsclient/resources/iconmonstr-gear-2-24-ubxlegacy.png b/src/pygpsclient/resources/iconmonstr-gear-2-24-ubxlegacy.png
new file mode 100644
index 00000000..6217de0f
Binary files /dev/null and b/src/pygpsclient/resources/iconmonstr-gear-2-24-ubxlegacy.png differ
diff --git a/src/pygpsclient/rinex_dialog.py b/src/pygpsclient/rinex_dialog.py
index 79ec08f1..8b86e226 100644
--- a/src/pygpsclient/rinex_dialog.py
+++ b/src/pygpsclient/rinex_dialog.py
@@ -30,6 +30,7 @@
Spinbox,
StringVar,
TclError,
+ Tk,
W,
ttk,
)
@@ -59,6 +60,13 @@
SBA,
)
+try:
+ from pygnssutils.rinex_globals import NMEA, RTCM3, UBLOX
+except ImportError:
+ NMEA = "nmea"
+ RTCM3 = "rtcm3"
+ UBLOX = "u-blox"
+
from pygpsclient.globals import (
CLICK_CURSOR,
ERRCOL,
@@ -85,6 +93,7 @@
LBLRINEXRCVR,
LBLRINEXSTARTTIME,
LBLRINEXSTATION,
+ LBLRINEXSVCODES,
LBLRINEXTYPES,
LBLRINEXVER,
NA,
@@ -96,10 +105,6 @@
RINEXVERSIONS = ("3.05", "4.02")
RINEXTYPES = (OBS, NAV, MET)
-NMEASOURCE = "NMEA 0183"
-RTCMSOURCE = "RTCM 3"
-UBXSOURCE = "UBX (u-blox)"
-RINEXSOURCES = {UBXSOURCE: "R", NMEASOURCE: "R", RTCMSOURCE: "N"}
RINEXMARKERTYPES = (
"GEODETIC",
"HUMAN",
@@ -124,18 +129,17 @@ class RINEXDialog(ToplevelDialog):
RINEXDialog class.
"""
- def __init__(self, app, *args, **kwargs): # pylint: disable=unused-argument
+ def __init__(self, app: Tk, *args, **kwargs): # pylint: disable=unused-argument
"""
Constructor.
- :param Frame app: reference to main tkinter application
+ :param Tk app: reference to main tkinter application
:param args: optional args to pass to parent class (not currently used)
:param kwargs: optional kwargs to pass to parent class (not currently used)
"""
self.__app = app # Reference to main application class
self.logger = getLogger(__name__)
- self.__master = self.__app.appmaster # Reference to root class (Tk)
super().__init__(app, DLGTRINEX)
self._infile_path = Path(".")
@@ -162,6 +166,7 @@ def __init__(self, app, *args, **kwargs): # pylint: disable=unused-argument
self._rxqzss = IntVar()
self._rxnavic = IntVar()
self._obstypes = StringVar()
+ self._svcodes = StringVar()
self._markernum = StringVar()
self._markername = StringVar()
self._markertype = StringVar()
@@ -262,7 +267,7 @@ def _body(self):
)
self._spn_obssource = Spinbox(
self._frm_basic,
- values=list(RINEXSOURCES),
+ values=(UBLOX,),
width=14,
wrap=True,
textvariable=self._obssource,
@@ -278,7 +283,7 @@ def _body(self):
)
self._spn_navsource = Spinbox(
self._frm_basic,
- values=list(RINEXSOURCES),
+ values=(UBLOX, RTCM3),
width=14,
wrap=True,
textvariable=self._navsource,
@@ -294,7 +299,7 @@ def _body(self):
)
self._spn_metsource = Spinbox(
self._frm_basic,
- values=list(RINEXSOURCES),
+ values=(NMEA,),
width=14,
wrap=True,
textvariable=self._metsource,
@@ -371,6 +376,13 @@ def _body(self):
state=NORMAL,
relief="sunken",
)
+ self._lbl_svcodes = Label(self._frm_basic, text=LBLRINEXSVCODES)
+ self._ent_svcodes = Entry(
+ self._frm_basic,
+ textvariable=self._svcodes,
+ state=NORMAL,
+ relief="sunken",
+ )
self._pgb_elapsed = Progressbar(
self._frm_basic,
orient="horizontal",
@@ -572,16 +584,20 @@ def _do_layout(self):
self._chk_rxnavic.grid(column=6, row=9, padx=3, sticky=W)
self._lbl_obstypes.grid(column=0, row=10, columnspan=7, padx=3, sticky=W)
self._ent_obstypes.grid(column=0, row=11, columnspan=7, padx=3, sticky=EW)
- self._lbl_corr.grid(column=0, row=12, columnspan=2, padx=3, sticky=W)
- self._chk_timecorr.grid(column=2, row=12, padx=3, sticky=W)
- self._chk_ionocorr.grid(column=3, row=12, padx=3, sticky=W)
- self._pgb_elapsed.grid(column=0, row=13, columnspan=7, padx=3, sticky=EW)
+
+ self._lbl_svcodes.grid(column=0, row=12, columnspan=7, padx=3, sticky=W)
+ self._ent_svcodes.grid(column=0, row=13, columnspan=7, padx=3, sticky=EW)
+
+ self._lbl_corr.grid(column=0, row=14, columnspan=2, padx=3, sticky=W)
+ self._chk_timecorr.grid(column=2, row=14, padx=3, sticky=W)
+ self._chk_ionocorr.grid(column=3, row=14, padx=3, sticky=W)
+ self._pgb_elapsed.grid(column=0, row=15, columnspan=7, padx=3, sticky=EW)
ttk.Separator(self._frm_basic).grid(
- column=0, row=14, columnspan=7, padx=3, sticky=EW
+ column=0, row=15, columnspan=7, padx=3, sticky=EW
)
- self._lbl_outputs.grid(column=0, row=15, columnspan=7, padx=3, pady=1, sticky=W)
+ self._lbl_outputs.grid(column=0, row=16, columnspan=7, padx=3, pady=1, sticky=W)
for i, lbl in enumerate(self._lbl_output_labels.values()):
- lbl.grid(column=0, row=16 + i, columnspan=7, padx=3, pady=1, sticky=EW)
+ lbl.grid(column=0, row=17 + i, columnspan=7, padx=3, pady=1, sticky=EW)
self._lbl_marker.grid(column=0, row=0, columnspan=5, padx=3, sticky=W)
self._ent_markernum.grid(column=0, row=1, padx=3, sticky=W)
@@ -620,8 +636,8 @@ def _do_layout_rinex4(self, show: bool, startrow: int):
"""
if show:
- self._chk_timecorr.config(text="STO")
- self._chk_ionocorr.config(text="ION")
+ self._chk_timecorr["text"] = "STO"
+ self._chk_ionocorr["text"] = "ION"
self._lbl_doi.grid(
column=0, row=startrow + 1, columnspan=5, padx=3, sticky=W
)
@@ -640,10 +656,10 @@ def _do_layout_rinex4(self, show: bool, startrow: int):
self._ent_station.grid(
column=0, row=startrow + 6, columnspan=5, padx=3, sticky=EW
)
- self._chk_eopcorr.grid(column=4, row=12, padx=3, sticky=W)
+ self._chk_eopcorr.grid(column=4, row=14, padx=3, sticky=W)
else:
- self._chk_timecorr.config(text="Time")
- self._chk_ionocorr.config(text="Iono")
+ self._chk_timecorr["text"] = "Time"
+ self._chk_ionocorr["text"] = "Iono"
self._lbl_doi.grid_forget()
self._ent_doi.grid_forget()
self._lbl_license.grid_forget()
@@ -690,9 +706,9 @@ def _reset(self):
self._rxnavic,
):
chk.set(1)
- self._obssource.set(UBXSOURCE)
- self._navsource.set(UBXSOURCE)
- self._metsource.set(NMEASOURCE)
+ self._obssource.set(UBLOX)
+ self._navsource.set(UBLOX)
+ self._metsource.set(NMEA)
self._antennah.set("0.0")
self._antennae.set("0.0")
self._antennad.set("0.0")
@@ -737,7 +753,7 @@ def _on_convert(self):
Run conversion.
"""
- self.status_label = f"Processing {self._infile_path.name}"
+ self.set_status_label(f"Processing {self._infile_path.name}")
for rt in RINEXTYPES:
self._outputlabels[rt].set("")
self._do_conversion()
@@ -773,7 +789,7 @@ def _on_load(self):
self._infilepath.set(sfp)
self._ent_infilepath.update()
validate(self._ent_infilepath, valmode=VALNONBLANK)
- self.status_label = (
+ self.set_status_label(
RINEXFILEVALIDATING.format(path=self._infile_path.name),
OKCOL,
)
@@ -785,19 +801,19 @@ def _on_load(self):
if raw is not None:
i += 1
if i == 0:
- self.status_label = (
+ self.set_status_label(
RINEXFILEINVALID.format(path=self._infile_path.name),
ERRCOL,
)
else:
- self.status_label = (
+ self.set_status_label(
RINEXFILEVALID.format(path=self._infile_path.name, count=i),
INFOCOL,
)
self._filecount = i
except Exception as err: # pylint: disable=broad-exception-caught
self.logger.error(f"Error processing {self._infile_path} - {err}")
- self.status_label = (f"Error processing {self._infile_path.name}", ERRCOL)
+ self.set_status_label(f"Error processing {self._infile_path.name}", ERRCOL)
def _toggle_advanced(self):
"""
@@ -807,10 +823,10 @@ def _toggle_advanced(self):
self._show_advanced = not self._show_advanced
if self._show_advanced:
self._frm_advanced.grid(column=1, row=0, sticky=NSEW)
- self._btn_toggle.config(image=self._img_contracth)
+ self._btn_toggle["image"] = self._img_contracth
else:
self._frm_advanced.grid_forget()
- self._btn_toggle.config(image=self._img_expandh)
+ self._btn_toggle["image"] = self._img_expandh
# def _valid_settings(self) -> bool:
# """
@@ -822,7 +838,7 @@ def _toggle_advanced(self):
# valid = True
# if not valid:
- # self.status_label = ("ERROR - invalid settings", ERRCOL)
+ # self.set_status_label("ERROR - invalid settings", ERRCOL)
# return valid
@@ -860,60 +876,63 @@ def _do_conversion(self, **kwargs):
valid = valid & validate(self._ent_antennae, valmode=VALFLOAT)
valid = valid & validate(self._ent_antennad, valmode=VALFLOAT)
if not valid:
- self.status_label = ("Invalid Parameters", ERRCOL)
+ self.set_status_label("Invalid Parameters", ERRCOL)
return
rinex_version = self._rinexver.get()
- rinex_types = []
+ rinex_types = ()
if self._rinexobs.get():
- rinex_types.append(OBS)
+ rinex_types += (OBS,)
if self._rinexnav.get():
- rinex_types.append(NAV)
+ rinex_types += (NAV,)
if self._rinexmet.get():
- rinex_types.append(MET)
- if rinex_types == []:
- self.status_label = ("Select at least one RINEX output type", ERRCOL)
+ rinex_types += (MET,)
+ if rinex_types == ():
+ self.set_status_label("Select at least one RINEX output type", ERRCOL)
valid = False
- gnss_filter = []
+ gnss_filter = ()
if self._rxgps.get():
- gnss_filter.append(GPS)
+ gnss_filter += (GPS,)
if self._rxgalileo.get():
- gnss_filter.append(GAL)
+ gnss_filter += (GAL,)
if self._rxglonass.get():
- gnss_filter.append(GLO)
+ gnss_filter += (GLO,)
if self._rxbeidou.get():
- gnss_filter.append(BDS)
+ gnss_filter += (BDS,)
if self._rxqzss.get():
- gnss_filter.append(QZS)
+ gnss_filter += (QZS,)
if self._rxnavic.get():
- gnss_filter.append(IRN)
+ gnss_filter += (IRN,)
if self._rxsbas.get():
- gnss_filter.append(SBA)
- if gnss_filter == []:
- self.status_label = ("Select at least one GNSS", ERRCOL)
+ gnss_filter += (SBA,)
+ if len(gnss_filter) == 7:
+ gnss_filter = ("",)
+ if gnss_filter == ():
+ self.set_status_label("Select at least one GNSS", ERRCOL)
valid = False
if not valid:
return
- obs_filter = self._obstypes.get().split(",")
+ obs_filter = tuple(self._obstypes.get().split(","))
+ sv_filter = tuple(self._svcodes.get().split(","))
starttime = self._starttime.get()
minobs = 10
- marker = [
+ marker = (
self._markername.get(),
self._markernum.get(),
self._markertype.get(),
- ]
- antenna = [self._antennanum.get(), self._antennatype.get()]
- antennahed = [
+ )
+ antenna = (self._antennanum.get(), self._antennatype.get())
+ antennahed = (
self._antennah.get(),
self._antennae.get(),
self._antennad.get(),
- ]
- receiver = [
+ )
+ receiver = (
self._rcvrnum.get(),
self._rcvrname.get(),
self._rcvrversion.get(),
- ]
+ )
observer = self._observer.get()
timecorr = self._timecorrflag.get()
ionocorr = self._ionocorrflag.get()
@@ -921,25 +940,27 @@ def _do_conversion(self, **kwargs):
doi = self._doi.get()
licen = self._license.get()
station = self._station.get()
- comments = ["PyGPSClient RINEX Converter Dialog"]
+ comments = ("PyGPSClient RINEX Converter Dialog",)
country = self._countrycode.get()
for cvar in self._usercommentvar:
cval = cvar.get()
if cval != "":
- comments.append(cval)
+ comments += (cval,)
protfilter = NMEA_PROTOCOL | RTCM3_PROTOCOL | UBX_PROTOCOL
- datasource = [
- RINEXSOURCES[self._obssource.get()],
- RINEXSOURCES[self._navsource.get()],
- RINEXSOURCES[self._metsource.get()],
- ]
+ obssource = self._obssource.get()
+ navsource = self._navsource.get()
+ metsource = self._metsource.get()
+
rc = RinexConverter(
self.__app,
rinex_version=rinex_version,
rinex_types=rinex_types,
gnssfilter=gnss_filter,
obsfilter=obs_filter,
- datasource=datasource,
+ svfilter=sv_filter,
+ obssource=obssource,
+ navsource=navsource,
+ metsource=metsource,
starttime=starttime,
minobs=minobs,
marker=marker,
@@ -956,6 +977,7 @@ def _do_conversion(self, **kwargs):
license=licen,
station=station,
country=country,
+ datasource=["R", "R", "R"], # for pygnssutils <= 1.3.5
**kwargs,
)
self._stopevent.clear()
@@ -973,7 +995,7 @@ def _do_conversion(self, **kwargs):
rct.start()
except (TclError, KeyboardInterrupt):
- self.status_label = ("Conversion Failed", ERRCOL)
+ self.set_status_label("Conversion Failed", ERRCOL)
def _process_input(
self,
@@ -1006,19 +1028,19 @@ def _process_input(
f"{rt}: {outputs[rt][0].name}: {outputs[rt][1]}"
)
if tot > 1:
- self.status_label = ("Conversion successful", OKCOL)
+ self.set_status_label("Conversion successful", OKCOL)
else:
- self.status_label = ("No usable information in input", ERRCOL)
+ self.set_status_label("No usable information in input", ERRCOL)
parts = self._infile_path.parts
path = f" (...{parts[-2]}/) " if len(parts) > 1 else " (.../)"
- self._lbl_outputs.config(text=LBLRINEXOUTPUTS.format(path=path))
+ self._lbl_outputs["text"] = LBLRINEXOUTPUTS.format(path=path)
elif res == RINEX_CANCELLED:
- self.status_label = ("Conversion cancelled", ERRCOL)
+ self.set_status_label("Conversion cancelled", ERRCOL)
elif res == RINEX_NORECS:
- self.status_label = ("No parsable records in input", ERRCOL)
+ self.set_status_label("No parsable records in input", ERRCOL)
else:
- self.status_label = ("Conversion failed - see log", ERRCOL)
+ self.set_status_label("Conversion failed - see log", ERRCOL)
def _prog_callback_threaded(self, progress: int):
"""
diff --git a/src/pygpsclient/rover_frame.py b/src/pygpsclient/rover_frame.py
index cc89a924..f48cb301 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 NSEW, NW, SW, Frame
+from tkinter import EW, NSEW, NW, SW, Frame, Label, Spinbox, StringVar, Tk, W
from pygpsclient.canvas_subclasses import (
MODE_POL,
@@ -28,12 +28,15 @@
from pygpsclient.globals import (
BGCOL,
PNTCOL,
+ READONLY,
+ TRACEMODE_WRITE,
WIDGETU2,
)
from pygpsclient.helpers import setubxrate
+from pygpsclient.rtcm3_handler import RTCM56
from pygpsclient.strings import DLGWAITRELPOS, NA
-MAXPOINTS = 100
+MAXPOINTS = 500
INSET = 4
TRKCOL = "#CD6600"
TRKTOL = 5
@@ -46,23 +49,23 @@ class RoverFrame(Frame):
Rover view frame class.
"""
- def __init__(self, app: Frame, parent: Frame, *args, **kwargs):
+ def __init__(self, app: Tk, parent: Frame, *args, **kwargs):
"""
Constructor.
- :param Frame app: reference to main tkinter application
+ :param Tk app: reference to main tkinter application
:param Frame parent: reference to parent frame
:param args: Optional args to pass to Frame parent class
:param kwargs: Optional kwargs to pass to Frame parent class
"""
self.__app = app
- self.__master = self.__app.appmaster
super().__init__(parent, *args, **kwargs)
def_w, def_h = WIDGETU2
self.width = kwargs.get("width", def_w)
self.height = kwargs.get("height", def_h)
+ self._source = StringVar()
self._range = 1
self._temprange = 1
self._scale_c = 1
@@ -72,6 +75,7 @@ def __init__(self, app: Frame, parent: Frame, *args, **kwargs):
self._maxdist = 0
self._waiting = True
self._body()
+ self.reset()
self._attach_events()
self.enable_messages(True)
@@ -83,7 +87,33 @@ def _body(self):
self._canvas = CanvasCompass(
self.__app, self, MODE_POL, width=self.width, height=self.height, bg=BGCOL
)
+ self._frm_options = Frame(self, bg=BGCOL)
+ self._lbl_options = Label(self._frm_options, text="Source", fg=PNTCOL, bg=BGCOL)
+ self._spn_options = Spinbox(
+ self._frm_options,
+ values=["NAV-RELPOSNED", "NAV-DAHEADING", RTCM56],
+ width=16,
+ wrap=True,
+ background=BGCOL,
+ readonlybackground=BGCOL,
+ foreground=PNTCOL,
+ buttonbackground=BGCOL,
+ textvariable=self._source,
+ state=READONLY,
+ )
self._canvas.grid(column=0, row=0, sticky=NSEW)
+ self._frm_options.grid(column=0, row=1, sticky=EW)
+ self._lbl_options.grid(column=0, row=0, sticky=W)
+ self._spn_options.grid(column=1, row=0, sticky=EW)
+
+ def reset(self):
+ """
+ Reset settings to saved configuration.
+ """
+
+ self._source.set(
+ self.__app.configuration.get("relposnedsettings_d")["source_s"]
+ )
def _attach_events(self):
"""
@@ -92,6 +122,7 @@ def _attach_events(self):
self.bind("", self._on_resize)
self._canvas.bind("", self._on_clear)
+ self._source.trace_add(TRACEMODE_WRITE, self._on_source)
def init_frame(self):
"""
@@ -105,7 +136,7 @@ def init_frame(self):
self._canvas.create_compass(
radii=range(maxgrid, 1, -2),
unit=self._scale_u,
- dp=0,
+ dp=1 if self._scale_u in ("m", "cm") else 0,
scale=scale,
tags=tags,
)
@@ -116,6 +147,11 @@ def update_frame(self):
Collect relative position data and update the plot.
"""
+ src = self.__app.gnss_status.rel_pos_source
+ if src != self._source.get():
+ self._on_clear(None)
+ return
+
center_x = self.width / 2
center_y = self.height / 2
hdg = self.__app.gnss_status.rel_pos_heading
@@ -160,7 +196,7 @@ def update_frame(self):
self._canvas.create_text(
INSET,
INSET,
- text=f"Len {dis:,.2f} ± {accdis:.2f} cm",
+ text=f"Src {self.__app.gnss_status.rel_pos_source}",
fill=PNTCOL,
anchor=NW,
font=self._canvas.font,
@@ -169,6 +205,15 @@ def update_frame(self):
self._canvas.create_text(
INSET,
INSET + fh,
+ text=f"Len {dis:,.2f} ± {accdis:.2f} cm",
+ fill=PNTCOL,
+ anchor=NW,
+ font=self._canvas.font,
+ tags=TAG_DATA,
+ )
+ self._canvas.create_text(
+ INSET,
+ INSET + fh * 2,
text=f"Hdg {hdg:.2f} ± {acchdg:.2f}",
fill=PNTCOL,
anchor=NW,
@@ -202,7 +247,6 @@ 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)
@@ -224,6 +268,8 @@ def update_frame(self):
)
self._canvas.create_circle(x, y, 3, fill=PNTCOL, outline=PNTCOL, tags=TAG_DATA)
+ self.update_idletasks()
+
def _set_range(self, distance: float):
"""
Set range and scale.
@@ -288,7 +334,21 @@ def enable_messages(self, status: int):
:param int status: 0 = off, 1 = on
"""
- setubxrate(self.__app, "NAV-RELPOSNED", status)
+ source = self.__app.configuration.get("relposnedsettings_d")["source_s"]
+ if source in ("NAV-RELPOSNED", "NAV-DAHEADING"):
+ setubxrate(self.__app, source, status, "UBX")
+ elif source == RTCM56:
+ setubxrate(self.__app, "TYPE1006", status, "RTCM_3X_TYPE")
+
+ def _on_source(self, var, index, mode): # pylint: disable=unused-argument
+ """
+ Action on updating data source.
+ """
+
+ self.update()
+ self.__app.configuration.set(
+ "relposnedsettings_d", {"source_s": self._source.get()}
+ )
def _on_clear(self, event): # pylint: disable=unused-argument
""" "
diff --git a/src/pygpsclient/rtcm3_handler.py b/src/pygpsclient/rtcm3_handler.py
index de753008..f438cc78 100644
--- a/src/pygpsclient/rtcm3_handler.py
+++ b/src/pygpsclient/rtcm3_handler.py
@@ -19,6 +19,8 @@
from pygpsclient.dialog_state import DLGTSERVER
+RTCM56 = "RTCM 1005/6" # antenna reference point message used by Rover Plot
+
class RTCM3Handler:
"""
@@ -29,11 +31,10 @@ def __init__(self, app):
"""
Constructor.
- :param Frame app: reference to main tkinter application
+ :param Tk app: reference to main tkinter application
"""
self.__app = app # Reference to main application class
- self.__master = self.__app.appmaster # Reference to root class (Tk)
self.logger = logging.getLogger(__name__)
self._raw_data = None
@@ -70,6 +71,18 @@ def _process_1005(self, parsed: RTCMMessage):
self.__app.gnss_status.base_ecefx = parsed.DF025
self.__app.gnss_status.base_ecefy = parsed.DF026
self.__app.gnss_status.base_ecefz = parsed.DF027
+
+ # update Survey-In base station location
+ if self.__app.dialog(DLGTSERVER) is not None:
+ self.__app.dialog(DLGTSERVER).update_base_location()
+
+ if (
+ self.__app.configuration.get("relposnedsettings_d")["source_s"]
+ != RTCM56
+ ):
+ return
+
+ self.__app.gnss_status.rel_pos_source = RTCM56
lat2, lon2, _ = ecef2llh(parsed.DF025, parsed.DF026, parsed.DF027)
self.__app.gnss_status.rel_pos_length = (
haversine(
@@ -83,8 +96,6 @@ def _process_1005(self, parsed: RTCMMessage):
self.__app.gnss_status.rel_pos_heading = bearing(
lat2, lon2, self.__app.gnss_status.lat, self.__app.gnss_status.lon
)
- # update Survey-In base station location
- if self.__app.dialog(DLGTSERVER) is not None:
- self.__app.dialog(DLGTSERVER).update_base_location()
+
except (AttributeError, TypeError, ValueError):
pass
diff --git a/src/pygpsclient/sbf_handler.py b/src/pygpsclient/sbf_handler.py
index 5d2304e9..41fb6cce 100644
--- a/src/pygpsclient/sbf_handler.py
+++ b/src/pygpsclient/sbf_handler.py
@@ -37,11 +37,10 @@ def __init__(self, app):
"""
Constructor.
- :param Frame app: reference to main tkinter application
+ :param Tk app: reference to main tkinter application
"""
self.__app = app # Reference to main application class
- self.__master = self.__app.appmaster # Reference to root class (Tk)
self.logger = logging.getLogger(__name__)
self._cdb = 0
@@ -62,7 +61,9 @@ def process_data(self, raw_data: bytes, parsed_data: object):
return
if self.__app.gnss_status.version_data["hwversion"] == NA:
self.__app.gnss_status.version_data["hwversion"] = "Septentrio"
- self.__app.device_label = self.__app.gnss_status.version_data["hwversion"]
+ self.__app.set_device_label(
+ self.__app.gnss_status.version_data["hwversion"]
+ )
# self.logger.debug(f"data received {parsed_data.identity}")
if parsed_data.identity == "PVTGeodetic":
self._process_PVTGeodetic(parsed_data)
@@ -132,4 +133,4 @@ def _process_ReceiverSetup(self, data: SBFMessage):
)
self.__app.gnss_status.version_data["romversion"] = NA
self.__app.gnss_status.version_data["gnss"] = NA
- self.__app.device_label = self.__app.gnss_status.version_data["hwversion"]
+ self.__app.set_device_label(self.__app.gnss_status.version_data["hwversion"])
diff --git a/src/pygpsclient/scatter_frame.py b/src/pygpsclient/scatter_frame.py
index a70a1eaa..19ea3ecf 100644
--- a/src/pygpsclient/scatter_frame.py
+++ b/src/pygpsclient/scatter_frame.py
@@ -35,6 +35,7 @@
Spinbox,
StringVar,
TclError,
+ Tk,
)
try:
@@ -92,17 +93,16 @@ class ScatterViewFrame(Frame):
Scatterplot view frame class.
"""
- def __init__(self, app: Frame, parent: Frame, *args, **kwargs):
+ def __init__(self, app: Tk, parent: Frame, *args, **kwargs):
"""
Constructor.
- :param Frame app: reference to main tkinter application
+ :param Tk app: reference to main tkinter application
:param Frame parent: reference to parent frame
:param args: Optional args to pass to Frame parent class
:param kwargs: Optional kwargs to pass to Frame parent class
"""
self.__app = app
- self.__master = self.__app.appmaster
super().__init__(parent, *args, **kwargs)
diff --git a/src/pygpsclient/serialconfig_frame.py b/src/pygpsclient/serialconfig_frame.py
index dce6e599..188bab2f 100644
--- a/src/pygpsclient/serialconfig_frame.py
+++ b/src/pygpsclient/serialconfig_frame.py
@@ -40,6 +40,7 @@
Spinbox,
StringVar,
TclError,
+ Tk,
W,
)
@@ -94,11 +95,11 @@ class SerialConfigFrame(Frame):
Serial port configuration frame class.
"""
- def __init__(self, app: Frame, parent: Frame, *args, **kwargs):
+ def __init__(self, app: Tk, parent: Frame, *args, **kwargs):
"""
Constructor.
- :param Frame app: reference to main tkinter application
+ :param Tk app: reference to main tkinter application
:param Frame parent: reference to parent frame
:param args: optional args to pass to Frame parent class
:param kwargs: optional kwargs for value ranges, or to pass to Frame parent class
@@ -159,10 +160,10 @@ def _body(self):
)
self._scr_portv = Scrollbar(self._frm_basic, orient=VERTICAL)
self._scr_porth = Scrollbar(self._frm_basic, orient=HORIZONTAL)
- self._lbx_port.config(yscrollcommand=self._scr_portv.set)
- self._lbx_port.config(xscrollcommand=self._scr_porth.set)
- self._scr_portv.config(command=self._lbx_port.yview)
- self._scr_porth.config(command=self._lbx_port.xview)
+ self._lbx_port["yscrollcommand"] = self._scr_portv.set
+ self._lbx_port["xscrollcommand"] = self._scr_porth.set
+ self._scr_portv["command"] = self._lbx_port.yview
+ self._scr_porth["command"] = self._lbx_port.xview
self._lbl_bpsrate = Label(self._frm_basic, text="Rate bps")
self._spn_bpsrate = Spinbox(
self._frm_basic,
@@ -359,10 +360,10 @@ def _on_toggle_advanced(self):
self._show_advanced = not self._show_advanced
if self._show_advanced:
self._frm_advanced.grid(column=0, row=1, sticky=NSEW)
- self._btn_toggle.config(image=self._img_contract)
+ self._btn_toggle["image"] = self._img_contract
else:
self._frm_advanced.grid_forget()
- self._btn_toggle.config(image=self._img_expand)
+ self._btn_toggle["image"] = self._img_expand
def _get_ports(self):
"""
diff --git a/src/pygpsclient/serialconfig_lband_frame.py b/src/pygpsclient/serialconfig_lband_frame.py
index af07fd77..f5a26a32 100644
--- a/src/pygpsclient/serialconfig_lband_frame.py
+++ b/src/pygpsclient/serialconfig_lband_frame.py
@@ -13,7 +13,7 @@
# pylint: disable=unused-argument
-from tkinter import Frame
+from tkinter import Frame, Tk
from pygpsclient.serialconfig_frame import MSGMODED, PARITIES, SerialConfigFrame
@@ -23,11 +23,11 @@ class SerialConfigLbandFrame(SerialConfigFrame):
L-BAND Serial port configuration frame class.
"""
- def __init__(self, app: Frame, parent: Frame, *args, **kwargs):
+ def __init__(self, app: Tk, parent: Frame, *args, **kwargs):
"""
Constructor.
- :param Frame app: reference to main tkinter application
+ :param Tk app: reference to main tkinter application
:param Frame parent: reference to parent frame
:param args: optional args to pass to Frame parent class
:param kwargs: optional kwargs for value ranges, or to pass to Frame parent class
diff --git a/src/pygpsclient/serverconfig_dialog.py b/src/pygpsclient/serverconfig_dialog.py
index 9933e176..353191b9 100644
--- a/src/pygpsclient/serverconfig_dialog.py
+++ b/src/pygpsclient/serverconfig_dialog.py
@@ -38,6 +38,7 @@
Spinbox,
StringVar,
TclError,
+ Tk,
W,
)
from tkinter.ttk import Progressbar
@@ -162,11 +163,11 @@ class ServerConfigDialog(ToplevelDialog):
Server configuration dialog class.
"""
- def __init__(self, app, *args, **kwargs):
+ def __init__(self, app: Tk, *args, **kwargs):
"""
Constructor.
- :param Frame app: reference to main tkinter application
+ :param Tk app: reference to main tkinter application
:param args: optional args to pass to Frame parent class
:param kwargs: optional kwargs for value ranges, or to pass to Frame parent class
"""
@@ -492,10 +493,10 @@ def _reset(self):
pem = cfg.get("tlspempath_s")
if https and not Path(pem).exists():
err = DLGNOTLS.format(hostpem=pem)
- self.status_label = (err, ERRCOL)
+ self.set_status_label(err, ERRCOL)
self.logger.error(err)
cfg.set("sockhttps_b", 0)
- self._chk_https.config(state=DISABLED)
+ self._chk_https["state"] = DISABLED
https = 0
self.https.set(https)
self.after(5, lambda: self._lbl_publicip.config(text=publicip()))
@@ -581,10 +582,10 @@ def _set_advanced(self):
if self._show_advanced:
self._frm_advanced.grid(column=0, row=1, columnspan=5, sticky=EW)
- self._btn_toggle.config(image=self._img_contract)
+ self._btn_toggle["image"] = self._img_contract
else:
self._frm_advanced.grid_forget()
- self._btn_toggle.config(image=self._img_expand)
+ self._btn_toggle["image"] = self._img_expand
def set_status(self, status: int):
"""
@@ -607,9 +608,9 @@ def _on_socketserve(self, var, index, mode):
# pylint: disable=line-too-long
if self.valid_settings():
- self.status_label = ("", INFOCOL)
+ self.set_status_label("", INFOCOL)
else:
- self.status_label = ("ERROR - invalid entry", ERRCOL)
+ self.set_status_label("ERROR - invalid entry", ERRCOL)
return
# different receiver models support different RTCM3 output message cohorts
@@ -669,7 +670,7 @@ def _set_controls(self):
state = DISABLED
else:
state = READONLY if isinstance(wid, Spinbox) else NORMAL
- wid.config(state=state)
+ wid["state"] = state
for wid in (
self._ent_fixedlat,
self._ent_fixedlon,
@@ -679,14 +680,14 @@ def _set_controls(self):
state = DISABLED
else:
state = READONLY if self.base_mode.get() == BASE_SVIN else NORMAL
- wid.config(state=state)
+ wid["state"] = state
for wid in (self._btn_configure_base,):
if self._socket_serve.get():
state = DISABLED
else:
state = NORMAL
- wid.config(state=state)
- self._lbl_elapsed.config(text="")
+ wid["state"] = state
+ self._lbl_elapsed["text"] = ""
def _on_configure_base(self, *args, **kwargs): # pylint: disable=unused-argument
"""
@@ -694,7 +695,7 @@ def _on_configure_base(self, *args, **kwargs): # pylint: disable=unused-argumen
"""
if self.sock_mode.get() == SOCK_NTRIP:
- self.status_label = ""
+ self.set_status_label("")
self._quectel_restart = 0
self._config_receiver()
@@ -706,9 +707,9 @@ def _config_receiver(self):
# validate settings
if self.valid_settings():
- self.status_label = ("", INFOCOL)
+ self.set_status_label("", INFOCOL)
else:
- self.status_label = ("ERROR - invalid entry", ERRCOL)
+ self.set_status_label("ERROR - invalid entry", ERRCOL)
return
# set base station timing mode
@@ -736,9 +737,9 @@ def _config_receiver(self):
self.receiver_type.get() == SEPTENTRIO_MOSAIC
and self.base_mode == BASE_SVIN
):
- self.status_label = SEPTENTRIOSVIN # receiver surveying message
+ self.set_status_label(SEPTENTRIOSVIN) # receiver surveying message
if self.receiver_type.get() == QUECTEL_LGSERIES:
- self.status_label = QUECTELRST1 # receiver will restart message
+ self.set_status_label(QUECTELRST1) # receiver will restart message
# poll for confirmation that rcvr has restarted,
# then resend configuration commands a 2nd time
self._pending_confs[PQTMVER] = SERVERCONFIG
@@ -749,9 +750,9 @@ def _on_update_sockhost(self, var, index, mode):
"""
if self._ent_sockhost.validate(VALNONBLANK):
- self._chk_socketserve.config(state=NORMAL)
+ self._chk_socketserve["state"] = NORMAL
else:
- self._chk_socketserve.config(state=DISABLED)
+ self._chk_socketserve["state"] = DISABLED
return
self.__app.configuration.set("sockhost_s", self.sock_host.get())
@@ -761,9 +762,9 @@ def _on_update_sockport(self, var, index, mode):
"""
if self._ent_sockport.validate(VALINT, 1, MAXPORT):
- self._chk_socketserve.config(state=NORMAL)
+ self._chk_socketserve["state"] = NORMAL
else:
- self._chk_socketserve.config(state=DISABLED)
+ self._chk_socketserve["state"] = DISABLED
return
try:
@@ -808,9 +809,9 @@ def _on_update_fixedlat(self, var, index, mode):
try:
if self._ent_fixedlat.validate(VALFLOAT):
- self._btn_configure_base.config(state=NORMAL)
+ self._btn_configure_base["state"] = NORMAL
else:
- self._btn_configure_base.config(state=DISABLED)
+ self._btn_configure_base["state"] = DISABLED
return
self.__app.configuration.set(
"ntripcasterfixedlat_f", float(self.fixedlat.get())
@@ -825,9 +826,9 @@ def _on_update_fixedlon(self, var, index, mode):
try:
if self._ent_fixedlon.validate(VALFLOAT):
- self._btn_configure_base.config(state=NORMAL)
+ self._btn_configure_base["state"] = NORMAL
else:
- self._btn_configure_base.config(state=DISABLED)
+ self._btn_configure_base["state"] = DISABLED
return
self.__app.configuration.set(
"ntripcasterfixedlon_f", float(self.fixedlon.get())
@@ -842,9 +843,9 @@ def _on_update_fixedhae(self, var, index, mode):
try:
if self._ent_fixedhae.validate(VALFLOAT):
- self._btn_configure_base.config(state=NORMAL)
+ self._btn_configure_base["state"] = NORMAL
else:
- self._btn_configure_base.config(state=DISABLED)
+ self._btn_configure_base["state"] = DISABLED
return
self.__app.configuration.set(
"ntripcasterfixedalt_f", float(self.fixedhae.get())
@@ -889,12 +890,12 @@ def _on_update_sockmode(self, var, index, mode):
"""
if self.sock_mode.get() == SOCK_NTRIP:
- self._btn_toggle.config(state=NORMAL)
+ self._btn_toggle["state"] = NORMAL
self._show_advanced = True
self.__app.configuration.set("sockmode_b", 1)
self.sock_port.set(self.__app.configuration.get("sockportntrip_n"))
else:
- self._btn_toggle.config(state=DISABLED)
+ self._btn_toggle["state"] = DISABLED
self._show_advanced = False
self.__app.configuration.set("sockmode_b", 0)
self.sock_port.set(self.__app.configuration.get("sockport_n"))
@@ -908,11 +909,11 @@ def _on_update_https(self, var, index, mode):
pem = self.__app.configuration.get("tlspempath_s")
if self.https.get() and not Path(pem).exists():
err = DLGNOTLS.format(hostpem=pem)
- self.status_label = (err, ERRCOL)
+ self.set_status_label(err, ERRCOL)
self.logger.error(err)
self._attach_events(False)
self.https.set(0)
- self._chk_https.config(state=DISABLED)
+ self._chk_https["state"] = DISABLED
self._attach_events(True)
self.__app.configuration.set("sockhttps_b", self.https.get())
@@ -922,7 +923,7 @@ def _on_update_basemode(self, var, index, mode):
Set field visibility depending on base mode.
"""
- self.status_label = ""
+ self.set_status_label("")
if self.base_mode.get() == BASE_SVIN:
self.disable_nmea.set(True)
self._on_update_basemode_svin()
@@ -956,7 +957,7 @@ def _on_update_basemode_svin(self):
self._lbl_password.grid(column=0, row=8, padx=2, pady=1, sticky=E)
self._ent_password.grid(column=1, row=8, columnspan=2, padx=2, pady=1, sticky=W)
for wid in self._ent_fixedlat, self._ent_fixedlon, self._ent_fixedhae:
- wid.config(state=DISABLED)
+ wid["state"] = DISABLED
def _on_update_basemode_fixed(self):
"""
@@ -982,7 +983,7 @@ def _on_update_basemode_fixed(self):
self._pgb_elapsed.grid_forget()
self._lbl_elapsed.grid_forget()
for wid in self._ent_fixedlat, self._ent_fixedlon, self._ent_fixedhae:
- wid.config(state=NORMAL)
+ wid["state"] = NORMAL
self._set_coords(self.pos_mode.get())
def _on_update_basemode_disabled(self):
@@ -1020,9 +1021,9 @@ def _on_update_posmode(self, var, index, mode):
if self.pos_mode.get() == POS_LLH
else ("X (m)", "Y (m)", "Z (m)")
)
- self._lbl_fixedlat.config(text=lbls[0])
- self._lbl_fixedlon.config(text=lbls[1])
- self._lbl_fixedhae.config(text=lbls[2])
+ self._lbl_fixedlat["text"] = lbls[0]
+ self._lbl_fixedlon["text"] = lbls[1]
+ self._lbl_fixedhae["text"] = lbls[2]
self._set_coords(self.pos_mode.get())
self.__app.configuration.set("ntripcasterposmode_s", self.pos_mode.get())
@@ -1139,11 +1140,11 @@ def _on_quectel_restart(self):
"""
self._quectel_restart += 1
- self.status_label = ""
+ self.set_status_label("")
if self.base_mode.get() in (BASE_SVIN, BASE_FIXED):
# if first restart, send config commands a 2nd time
if self._quectel_restart == 1:
- self.status_label = QUECTELRST2 # receiver restart again message
+ self.set_status_label(QUECTELRST2) # receiver restart again message
self._config_receiver()
return
# if second restart and survey-in mode, enable SVIN status message
@@ -1163,7 +1164,7 @@ def svin_countdown(self, ela: int, valid: bool, active: bool):
"""
dur = self.duration.get()
- self.status_label = SVINSTART.format(100 * ela / dur)
+ self.after(0, self.set_status_label(SVINSTART.format(100 * ela / dur)))
if self.base_mode.get() == BASE_SVIN and active and not valid:
self._lbl_elapsed.grid_forget()
self._pgb_elapsed.grid(
@@ -1171,13 +1172,13 @@ def svin_countdown(self, ela: int, valid: bool, active: bool):
)
self._pgb_elapsed["value"] = 100 * (dur - ela) / dur
if ela >= dur:
- self.status_label = (SVINERR, ERRCOL)
+ self.set_status_label(SVINERR, ERRCOL)
elif self.base_mode.get() == BASE_SVIN and valid:
self._pgb_elapsed.grid_forget()
self._lbl_elapsed.grid(
column=2, row=2, columnspan=2, padx=2, pady=1, sticky=W
)
- self.status_label = (SVINOK, OKCOL)
+ self.set_status_label(SVINOK, OKCOL)
else:
self._lbl_elapsed.grid_forget()
self._pgb_elapsed.grid_forget()
@@ -1233,7 +1234,7 @@ def update_base_location(self):
"""
if self.base_mode.get() in (BASE_FIXED, BASE_SVIN):
- self.status_label = ""
+ self.set_status_label("")
if self.pos_mode.get() == POS_ECEF:
self.fixedlat.set(self.__app.gnss_status.base_ecefx)
self.fixedlon.set(self.__app.gnss_status.base_ecefy)
diff --git a/src/pygpsclient/settings_child_frame.py b/src/pygpsclient/settings_child_frame.py
index 110cb8cd..b174dbf5 100644
--- a/src/pygpsclient/settings_child_frame.py
+++ b/src/pygpsclient/settings_child_frame.py
@@ -33,6 +33,7 @@
Spinbox,
StringVar,
TclError,
+ Tk,
W,
ttk,
)
@@ -68,6 +69,7 @@
ICON_TRANSMIT,
ICON_TTYCONFIG,
ICON_UBXCONFIG,
+ ICON_UBXCONFIGLEGACY,
INFOCOL,
KNOWNGPS,
MSGMODES,
@@ -90,6 +92,7 @@
DLGTSERVER,
DLGTTTY,
DLGTUBX,
+ DLGTUBXLEGACY,
LBLAUTOSCROLL,
LBLDATABASERECORD,
LBLDATADISP,
@@ -103,10 +106,11 @@
LBLTRACKRECORD,
LBLTTYCONFIG,
LBLUBXCONFIG,
+ LBLUBXLEGACYCONFIG,
)
MAXLINES = ("200", "500", "1000", "2000", "100")
-FILEDELAYS = (2, 5, 10, 20, 50, 100, 200, 500, 1000, 2000)
+FILEDELAYS = (1, 2, 5, 10, 20, 50, 100, 200, 500, 1000, 2000) # ms
class SettingsChildFrame(Frame):
@@ -114,18 +118,17 @@ class SettingsChildFrame(Frame):
Settings child frame class.
"""
- def __init__(self, app: Frame, parent: Frame, *args, **kwargs):
+ def __init__(self, app: Tk, parent: Frame, *args, **kwargs):
"""
Constructor.
- :param Frame app: reference to main tkinter application
+ :param Tk app: reference to main tkinter application
:param Frame parent: reference to parent frame
:param args: optional args to pass to Frame parent class
:param kwargs: optional kwargs to pass to Frame parent class
"""
self.__app = app # Reference to main application class
- self.__master = self.__app.appmaster # Reference to root class (Tk)
self.__container = parent
super().__init__(parent, *args, **kwargs)
@@ -142,6 +145,7 @@ def __init__(self, app: Frame, parent: Frame, *args, **kwargs):
self._prot_rtcm = IntVar()
self._prot_spartn = IntVar()
self._prot_tty = IntVar()
+ self._prot_meta = IntVar()
self._autoscroll = IntVar()
self._maxlines = IntVar()
self._filedelay = IntVar()
@@ -161,6 +165,7 @@ def __init__(self, app: Frame, parent: Frame, *args, **kwargs):
self._img_disconn = ImageTk.PhotoImage(Image.open(ICON_DISCONN))
self._img_exit = ImageTk.PhotoImage(Image.open(ICON_POWEROFF))
self._img_ubxconfig = ImageTk.PhotoImage(Image.open(ICON_UBXCONFIG))
+ self._img_ubxconfiglegacy = ImageTk.PhotoImage(Image.open(ICON_UBXCONFIGLEGACY))
self._img_nmeaconfig = ImageTk.PhotoImage(Image.open(ICON_NMEACONFIG))
self._img_ttyconfig = ImageTk.PhotoImage(Image.open(ICON_TTYCONFIG))
self._img_ntripconfig = ImageTk.PhotoImage(Image.open(ICON_NTRIPCONFIG))
@@ -290,11 +295,16 @@ def _body(self):
text="TTY",
variable=self._prot_tty,
)
+ self._chk_meta = Checkbutton(
+ self._frm_options,
+ text="Meta Only",
+ variable=self._prot_meta,
+ )
self._lbl_consoledisplay = Label(self._frm_options, text=LBLDATADISP)
self._spn_conformat = Spinbox(
self._frm_options,
values=FORMATS,
- width=10,
+ width=15,
state=READONLY,
wrap=True,
textvariable=self._console_format,
@@ -381,6 +391,17 @@ def _body(self):
command=lambda: self.__app.start_dialog(DLGTUBX),
cursor=CLICK_CURSOR,
)
+ self._lbl_ubxlegacyconfig = Label(
+ self._frm_options_btns,
+ text=LBLUBXLEGACYCONFIG,
+ )
+ self._btn_ubxlegacyconfig = Button(
+ self._frm_options_btns,
+ width=45,
+ image=self._img_ubxconfiglegacy,
+ command=lambda: self.__app.start_dialog(DLGTUBXLEGACY),
+ cursor=CLICK_CURSOR,
+ )
self._lbl_nmeaconfig = Label(
self._frm_options_btns,
text=LBLNMEACONFIG,
@@ -472,6 +493,7 @@ def _do_layout(self):
self._chk_qgc.grid(column=3, row=1, padx=0, pady=0, sticky=W)
self._chk_spartn.grid(column=1, row=2, padx=0, pady=0, sticky=W)
self._chk_tty.grid(column=2, row=2, padx=0, pady=0, sticky=W)
+ self._chk_meta.grid(column=3, row=2, padx=0, pady=0, sticky=W)
self._lbl_consoledisplay.grid(column=0, row=3, padx=2, pady=2, sticky=W)
self._spn_conformat.grid(
column=1, row=3, columnspan=2, padx=1, pady=2, sticky=W
@@ -495,14 +517,16 @@ def _do_layout(self):
self._frm_options_btns.grid(column=0, row=10, columnspan=4, sticky=EW)
self._btn_ubxconfig.grid(column=0, row=0, padx=2, pady=1)
self._lbl_ubxconfig.grid(column=0, row=1)
- self._btn_nmeaconfig.grid(column=1, row=0, padx=2, pady=1)
- self._lbl_nmeaconfig.grid(column=1, row=1)
- self._btn_ttyconfig.grid(column=2, row=0, padx=2, pady=1)
- self._lbl_ttyconfig.grid(column=2, row=1)
- self._btn_ntripconfig.grid(column=3, row=0, padx=2, pady=1)
- self._lbl_ntripconfig.grid(column=3, row=1)
- self._btn_serverconfig.grid(column=4, row=0, padx=2, pady=1)
- self._lbl_serverconfig.grid(column=4, row=1)
+ self._btn_ubxlegacyconfig.grid(column=1, row=0, padx=2, pady=1)
+ self._lbl_ubxlegacyconfig.grid(column=1, row=1)
+ self._btn_nmeaconfig.grid(column=2, row=0, padx=2, pady=1)
+ self._lbl_nmeaconfig.grid(column=2, row=1)
+ self._btn_ttyconfig.grid(column=3, row=0, padx=2, pady=1)
+ self._lbl_ttyconfig.grid(column=3, row=1)
+ self._btn_ntripconfig.grid(column=4, row=0, padx=2, pady=1)
+ self._lbl_ntripconfig.grid(column=4, row=1)
+ self._btn_serverconfig.grid(column=5, row=0, padx=2, pady=1)
+ self._lbl_serverconfig.grid(column=5, row=1)
def _attach_events(self, add: bool = True):
"""
@@ -524,6 +548,7 @@ def _attach_events(self, add: bool = True):
self._prot_rtcm.trace_update(tracemode, self._on_update_rtcmprot, add)
self._prot_spartn.trace_update(tracemode, self._on_update_spartnprot, add)
self._prot_tty.trace_update(tracemode, self._on_update_ttyprot, add)
+ self._prot_meta.trace_update(tracemode, self._on_update_metaprot, add)
self._autoscroll.trace_update(tracemode, self._on_update_autoscroll, add)
self._maxlines.trace_update(tracemode, self._on_update_maxlines, add)
self._filedelay.trace_update(tracemode, self._on_update_filedelay, add)
@@ -551,6 +576,7 @@ def reset(self):
self._prot_rtcm.set(cfg.get("rtcmprot_b"))
self._prot_spartn.set(cfg.get("spartnprot_b"))
self._prot_tty.set(cfg.get("ttyprot_b"))
+ self._prot_meta.set(cfg.get("metaprot_b"))
self._degrees_format.set(cfg.get("degreesformat_s"))
self._colortag.set(cfg.get("colortag_b"))
self._units.set(cfg.get("units_s"))
@@ -568,7 +594,7 @@ def reset(self):
self._record_database.set(cfg.get("database_b"))
else:
self._record_database.set(0)
- self._chk_recorddatabase.config(state=DISABLED)
+ self._chk_recorddatabase["state"] = DISABLED
self.clients = self.__app.server_status
self.enable_controls(self.__app.conn_status)
@@ -649,6 +675,7 @@ def _on_update_ttyprot(self, var, index, mode):
self._prot_unicore,
self._prot_rtcm,
self._prot_spartn,
+ self._prot_meta,
):
wdg.set(0)
else:
@@ -659,10 +686,19 @@ def _on_update_ttyprot(self, var, index, mode):
self._prot_unicore.set(cfg.get("uniprot_b"))
self._prot_rtcm.set(cfg.get("rtcmprot_b"))
self._prot_spartn.set(cfg.get("spartnprot_b"))
+ self._prot_meta.set(cfg.get("metaprot_b"))
cfg.set("ttyprot_b", tty)
except (ValueError, TclError):
pass
+ def _on_update_metaprot(self, var, index, mode):
+ """
+ Action on updating meta only checkbox.
+ """
+
+ if not self._prot_tty.get():
+ self.__app.configuration.set("metaprot_b", self._prot_meta.get())
+
def _on_update_consoleformat(self, var, index, mode):
"""
Action on updating console format.
@@ -730,7 +766,7 @@ 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.status_label = (
+ self.__app.set_status_label(
f"Data logging enabled: {self.logpath}",
INFOCOL,
)
@@ -740,13 +776,13 @@ def _on_data_log(self, var, index, mode):
else:
self.logpath = ""
self._datalog.set(0)
- self._spn_datalog.config(state=DISABLED)
+ self._spn_datalog["state"] = DISABLED
else:
self.__app.configuration.set("datalog_b", 0)
self._datalog.set(0)
self.__app.file_handler.close_logfile()
- self.__app.status_label = ("Data logging disabled", INFOCOL)
- self._spn_datalog.config(state=READONLY)
+ self.__app.set_status_label("Data logging disabled", INFOCOL)
+ self._spn_datalog["state"] = READONLY
def _on_record_track(self, var, index, mode):
"""
@@ -759,7 +795,9 @@ 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.status_label = f"Track recording enabled: {self.trackpath}"
+ self.__app.set_status_label(
+ f"Track recording enabled: {self.trackpath}"
+ )
if not self.__app.file_handler.open_trackfile():
self.trackpath = ""
self._record_track.set(0)
@@ -770,7 +808,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.status_label = "Track recording disabled"
+ self.__app.set_status_label("Track recording disabled")
def _on_record_database(self, var, index, mode):
"""
@@ -789,7 +827,7 @@ def _on_record_database(self, var, index, mode):
self._record_database.set(0)
else:
self.__app.configuration.set("database_b", 0)
- self.__app.status_label = "Database recording disabled"
+ self.__app.set_status_label("Database recording disabled")
def _on_connect(self, conntype: int):
"""
@@ -805,7 +843,6 @@ def _on_connect(self, conntype: int):
"eof_event": GNSS_EOF_EVENT,
"timeout_event": GNSS_TIMEOUT_EVENT,
"error_event": GNSS_ERR_EVENT,
- "inqueue": self.__app.gnss_inqueue,
"outqueue": self.__app.gnss_outqueue,
"socket_inqueue": self.__app.socket_inqueue,
"conntype": conntype,
@@ -814,6 +851,11 @@ def _on_connect(self, conntype: int):
"tlscrtpath": self.__app.configuration.get("tlscrtpath_s"),
}
+ if conntype == DISCONNECTED:
+ if self.__app.conn_status != DISCONNECTED:
+ self.__app.conn_status = DISCONNECTED
+ return
+
if conntype == CONNECTED:
frm = self.frm_serial
if frm.status == NOPORTS:
@@ -825,7 +867,7 @@ def _on_connect(self, conntype: int):
elif conntype == CONNECTED_SOCKET:
frm = self.frm_socketclient
if not frm.valid_settings():
- self.__app.status_label = ("ERROR - invalid settings", ERRCOL)
+ self.__app.set_status_label("ERROR - invalid settings", ERRCOL)
return
connstr = f"{frm.server.get()}:{frm.port.get()}"
conndict = dict(conndict, **{"socket_settings": frm})
@@ -845,17 +887,10 @@ def _on_connect(self, conntype: int):
return
connstr = f"{self.infilepath}"
conndict = dict(conndict, **{"in_filepath": self.infilepath})
- elif conntype == DISCONNECTED:
- if self.__app.conn_status != DISCONNECTED:
- self.__app.conn_status = DISCONNECTED
- self.__app.stream_handler.stop()
- return
- else:
- return
self.__app.conn_status = conntype
- self.__app.conn_label = (connstr, OKCOL)
- self.__app.status_label = ("", INFOCOL)
+ self.__app.set_conn_label(connstr, OKCOL)
+ self.__app.set_status_label("", INFOCOL)
self.__app.last_map_update = 0 # reset MAPQuest API refresh clock
self.__app.gnss_status.reset() # reset all GNSS data
self.__app.stream_handler.start(self.__app, conndict)
@@ -874,27 +909,24 @@ def enable_controls(self, status: int):
self.frm_serial.status_label = status
self.frm_socketclient.status_label = status
- self._btn_connect.config(
- state=(
- DISABLED
- if status in (CONNECTED, CONNECTED_SOCKET, CONNECTED_FILE, NOPORTS)
- else NORMAL
- )
+ self._btn_connect["state"] = (
+ DISABLED
+ if status in (CONNECTED, CONNECTED_SOCKET, CONNECTED_FILE, NOPORTS)
+ else NORMAL
)
for ctl in (
self._btn_connect_socket,
self._btn_connect_file,
self._chk_tty,
+ self._chk_meta,
):
- ctl.config(
- state=(
- DISABLED
- if status in (CONNECTED, CONNECTED_SOCKET, CONNECTED_FILE)
- else NORMAL
- )
+ ctl["state"] = (
+ DISABLED
+ if status in (CONNECTED, CONNECTED_SOCKET, CONNECTED_FILE)
+ else NORMAL
)
- self._btn_disconnect.config(
- state=(DISABLED if status in (DISCONNECTED,) else NORMAL)
+ self._btn_disconnect["state"] = (
+ DISABLED if status in (DISCONNECTED,) else NORMAL
)
def get_size(self) -> tuple:
diff --git a/src/pygpsclient/settings_dialog.py b/src/pygpsclient/settings_dialog.py
index 5032e567..7657f288 100644
--- a/src/pygpsclient/settings_dialog.py
+++ b/src/pygpsclient/settings_dialog.py
@@ -12,7 +12,7 @@
:license: BSD 3-Clause
"""
-from tkinter import NSEW, Frame
+from tkinter import NSEW, Tk
from pygpsclient.settings_child_frame import SettingsChildFrame
from pygpsclient.strings import DLGTSETTINGS
@@ -24,17 +24,16 @@ class SettingsDialog(ToplevelDialog):
Settings frame class.
"""
- def __init__(self, app: Frame, *args, **kwargs):
+ def __init__(self, app: Tk, *args, **kwargs):
"""
Constructor.
- :param Frame app: reference to main tkinter application
+ :param Tk app: reference to main tkinter application
:param args: optional args to pass to Frame parent class
:param kwargs: optional kwargs to pass to Frame parent class
"""
self.__app = app # Reference to main application class
- self.__master = self.__app.appmaster # Reference to root class (Tk)
super().__init__(app, DLGTSETTINGS)
diff --git a/src/pygpsclient/settings_frame.py b/src/pygpsclient/settings_frame.py
index fe12d1f2..8b8d4735 100644
--- a/src/pygpsclient/settings_frame.py
+++ b/src/pygpsclient/settings_frame.py
@@ -12,7 +12,7 @@
:license: BSD 3-Clause
"""
-from tkinter import Frame
+from tkinter import Frame, Tk
from pygpsclient.canvas_subclasses import CanvasContainer
from pygpsclient.settings_child_frame import SettingsChildFrame
@@ -23,20 +23,18 @@ class SettingsFrame(Frame):
Settings frame class.
"""
- def __init__(self, app: Frame, parent: Frame, *args, **kwargs):
+ def __init__(self, app: Tk, *args, **kwargs):
"""
Constructor.
- :param Frame app: reference to main tkinter application
- :param Frame parent: reference to parent frame
+ :param Tk app: reference to main tkinter application
:param args: optional args to pass to Frame parent class
:param kwargs: optional kwargs to pass to Frame parent class
"""
self.__app = app # Reference to main application class
- self.__master = self.__app.appmaster # Reference to root class (Tk)
- super().__init__(parent, *args, **kwargs)
+ super().__init__(app, *args, **kwargs)
self._container() # create scrollable container
self._body()
@@ -79,10 +77,8 @@ def _do_layout(self):
# resize container canvas to accommodate frame
self._frm_container.update()
- self._can_container.config(
- height=self._frm_container.winfo_height(),
- width=self._frm_container.winfo_width(),
- )
+ self._can_container["height"] = self._frm_container.winfo_height()
+ self._can_container["width"] = self._frm_container.winfo_width()
self._can_container.update()
def get_size(self) -> tuple:
diff --git a/src/pygpsclient/signalsview_frame.py b/src/pygpsclient/signalsview_frame.py
index 42775d11..18b46bc1 100644
--- a/src/pygpsclient/signalsview_frame.py
+++ b/src/pygpsclient/signalsview_frame.py
@@ -15,7 +15,7 @@
# pylint: disable=no-member, unused-variable, duplicate-code
-from tkinter import ALL, NSEW, NW, SE, Frame, N, S, font
+from tkinter import ALL, NSEW, NW, SE, Frame, N, S, Tk, font
from pyubx2 import CORRSOURCE, SIGID, UBXMessage
@@ -69,18 +69,17 @@ class SignalsviewFrame(Frame):
Signalsview frame class.
"""
- def __init__(self, app: Frame, parent: Frame, *args, **kwargs):
+ def __init__(self, app: Tk, parent: Frame, *args, **kwargs):
"""
Constructor.
- :param Frame app: reference to main tkinter application
+ :param Tk app: reference to main tkinter application
:param Frame parent: reference to parent frame
:param args: optional args to pass to Frame parent class
:param kwargs: optional kwargs to pass to Frame parent class
"""
self.__app = app # Reference to main application class
- self.__master = self.__app.appmaster # Reference to root class (Tk)
super().__init__(parent, *args, **kwargs)
@@ -338,10 +337,10 @@ def update_frame(self):
tags=TAG_DATA,
)
offset += colwidth
+ self.update_idletasks()
if self.__app.configuration.get("legend_b"):
self._draw_legend()
- self.update_idletasks()
def _on_resize(self, event): # pylint: disable=unused-argument
"""
diff --git a/src/pygpsclient/skyview_frame.py b/src/pygpsclient/skyview_frame.py
index 515842eb..03a0b92f 100644
--- a/src/pygpsclient/skyview_frame.py
+++ b/src/pygpsclient/skyview_frame.py
@@ -14,7 +14,7 @@
# pylint: disable = no-member
-from tkinter import NSEW, Frame
+from tkinter import NSEW, Frame, Tk
from pygpsclient.canvas_subclasses import (
MODE_CEL,
@@ -42,18 +42,17 @@ class SkyviewFrame(Frame):
Skyview frame class.
"""
- def __init__(self, app: Frame, parent: Frame, *args, **kwargs):
+ def __init__(self, app: Tk, parent: Frame, *args, **kwargs):
"""
Constructor.
- :param Frame app: reference to main tkinter application
+ :param Tk app: reference to main tkinter application
:param Frame parent: reference to parent frame
:param args: optional args to pass to Frame parent class
:param kwargs: optional kwargs to pass to Frame parent class
"""
self.__app = app # Reference to main application class
- self.__master = self.__app.appmaster # Reference to root class (Tk)
super().__init__(parent, *args, **kwargs)
@@ -145,11 +144,10 @@ def update_frame(self):
font=self._canvas.font,
tags=TAG_DATA,
)
+ self.update_idletasks()
except ValueError:
pass
- self.update_idletasks()
-
def _on_resize(self, event): # pylint: disable=unused-argument
"""
Resize frame
diff --git a/src/pygpsclient/socketconfig_frame.py b/src/pygpsclient/socketconfig_frame.py
index 341c7499..421c3525 100644
--- a/src/pygpsclient/socketconfig_frame.py
+++ b/src/pygpsclient/socketconfig_frame.py
@@ -28,6 +28,7 @@
Spinbox,
StringVar,
TclError,
+ Tk,
W,
)
@@ -61,7 +62,7 @@ class SocketConfigFrame(Frame):
Socket configuration frame class.
"""
- def __init__(self, app: Frame, parent: Frame, *args, **kwargs):
+ def __init__(self, app: Tk, parent: Frame, *args, **kwargs):
"""
Constructor.
diff --git a/src/pygpsclient/socketconfig_ntrip_frame.py b/src/pygpsclient/socketconfig_ntrip_frame.py
index fe69e444..b7381805 100644
--- a/src/pygpsclient/socketconfig_ntrip_frame.py
+++ b/src/pygpsclient/socketconfig_ntrip_frame.py
@@ -13,7 +13,7 @@
# pylint: disable=unused-argument
-from tkinter import Frame, TclError
+from tkinter import Frame, TclError, Tk
from pygpsclient.globals import DEFAULT_TLS_PORTS, VALINT, VALURL
from pygpsclient.helpers import MAXPORT
@@ -25,11 +25,11 @@ class SocketConfigNtripFrame(SocketConfigFrame):
Socket configuration frame class.
"""
- def __init__(self, app: Frame, parent: Frame, *args, **kwargs):
+ def __init__(self, app: Tk, parent: Frame, *args, **kwargs):
"""
Constructor.
- :param tkinter.Frame container: reference to container frame
+ :param Tk app: reference to container frame
:param Frame parent: reference to parent frame
:param args: optional args to pass to Frame parent class
:param kwargs: optional kwargs for value ranges, or to pass to Frame parent class
diff --git a/src/pygpsclient/spartn_dialog.py b/src/pygpsclient/spartn_dialog.py
index 2781f17b..21006474 100644
--- a/src/pygpsclient/spartn_dialog.py
+++ b/src/pygpsclient/spartn_dialog.py
@@ -48,13 +48,12 @@ def __init__(self, app, *args, **kwargs): # pylint: disable=unused-argument
"""
Constructor.
- :param Frame app: reference to main tkinter application
+ :param Tk app: reference to main tkinter application
:param args: optional args to pass to parent class (not currently used)
:param kwargs: optional kwargs to pass to parent class (not currently used)
"""
self.__app = app # Reference to main application class
- self.__master = self.__app.appmaster # Reference to root class (Tk)
super().__init__(app, DLGTSPARTN)
self._pending_confs = {}
@@ -186,7 +185,7 @@ def set_controls(self, status: bool, msgt: tuple | NoneType = None):
elif status == CONNECTED_SPARTNLB:
self.frm_corrlband.set_controls(status)
if msgt is not None:
- self.status_label = msgt
+ self.set_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 db4d46dd..46dfde15 100644
--- a/src/pygpsclient/spartn_gnss_frame.py
+++ b/src/pygpsclient/spartn_gnss_frame.py
@@ -30,6 +30,7 @@
Radiobutton,
Spinbox,
StringVar,
+ Tk,
W,
ttk,
)
@@ -103,18 +104,17 @@ class SPARTNGNSSDialog(Frame):
SPARTNConfigDialog class.
"""
- def __init__(self, app: Frame, parent: Frame, *args, **kwargs):
+ def __init__(self, app: Tk, parent: Frame, *args, **kwargs):
"""
Constructor.
- :param Frame app: reference to main tkinter application
+ :param Tk app: reference to main tkinter application
:param Frame container: reference to container frame
:param args: optional args to pass to parent class (not currently used)
:param kwargs: optional kwargs to pass to parent class (not currently used)
"""
self.__app = app # Reference to main application class
- self.__master = self.__app.appmaster # Reference to root class (Tk)
self.__container = parent # container frame
super().__init__(parent.container, *args, **kwargs)
@@ -450,7 +450,7 @@ def _on_send_gnss_config(self):
else:
msga = ""
self.__container.status_label = f"{(msgk + msga + msgc).capitalize()} sent"
- self._lbl_send_command.config(image=self._img_pending)
+ self._lbl_send_command["image"] = self._img_pending
for msgid in ("RXM-SPARTNKEY", "CFG-VALGET", "ACK-ACK", "ACK-NAK"):
self.__container.set_pending(msgid, SPARTN_GNSS)
@@ -492,7 +492,7 @@ def update_status(self, msg: UBXMessage):
"""
if msg.identity == RXMMSG:
- self._lbl_send_command.config(image=self._img_confirmed)
+ self._lbl_send_command["image"] = self._img_confirmed
if msg.numKeys == 2:
keydata = parse_rxmspartnkey(msg) # key1, date1, key2, date2
self._spartn_key1.set(keydata[0][0])
@@ -506,10 +506,10 @@ def update_status(self, msg: UBXMessage):
col = ERRCOL
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._lbl_send_command["image"] = self._img_confirmed
self.__container.status_label = (CONFIGOK.format(CFGSET), OKCOL)
elif msg.identity == "ACK-NAK":
- self._lbl_send_command.config(image=self._img_warn)
+ self._lbl_send_command["image"] = self._img_warn
self.__container.status_label = (CONFIGBAD.format(CFGSET), ERRCOL)
self.update_idletasks()
diff --git a/src/pygpsclient/spartn_lband_frame.py b/src/pygpsclient/spartn_lband_frame.py
index f3448bd4..5da6832f 100644
--- a/src/pygpsclient/spartn_lband_frame.py
+++ b/src/pygpsclient/spartn_lband_frame.py
@@ -29,6 +29,7 @@
Spinbox,
StringVar,
TclError,
+ Tk,
W,
ttk,
)
@@ -131,18 +132,17 @@ class SpartnLbandDialog(Frame):
SPARTNConfigDialog class.
"""
- def __init__(self, app: Frame, parent: Frame, *args, **kwargs):
+ def __init__(self, app: Tk, parent: Frame, *args, **kwargs):
"""
Constructor.
- :param Frame app: reference to main tkinter application
+ :param Tk app: reference to main tkinter application
:param Frame parent: reference to parent frame
:param args: optional args to pass to parent class (not currently used)
:param kwargs: optional kwargs to pass to parent class (not currently used)
"""
self.__app = app # Reference to main application class
- self.__master = self.__app.appmaster # Reference to root class (Tk)
self.__container = parent # container frame
super().__init__(parent.container, *args, **kwargs)
@@ -351,7 +351,7 @@ def _attach_events(self):
Bind events to frame.
"""
- self.__master.bind(SPARTN_ERR_EVENT, self.on_error)
+ self.__app.bind(SPARTN_ERR_EVENT, self.on_error)
for setting in (
self._enabledbg,
self._spartn_freq,
@@ -423,7 +423,7 @@ def set_controls(self, status: int):
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)
+ wdg["state"] = stat
stat = NORMAL if status == CONNECTED_SPARTNLB else DISABLED
for wdg in (
self._ent_freq,
@@ -439,13 +439,13 @@ def set_controls(self, status: int):
self._btn_disconnect,
self._btn_send,
):
- wdg.config(state=stat)
+ wdg["state"] = stat
stat = READONLY if status == CONNECTED_SPARTNLB else DISABLED
for wdg in (
self._spn_drat,
self._spn_outport,
):
- wdg.config(state=stat)
+ wdg["state"] = stat
def _valid_settings(self) -> bool:
"""
@@ -627,7 +627,7 @@ def _on_send_config(self):
msg = self._format_cfgcorr()
self._send_command(msg)
self.__container.status_label = f"{CFGSET} command sent"
- self._lbl_send.config(image=self._img_pending)
+ self._lbl_send["image"] = self._img_pending
# save config to persistent memory
if self._saveconfig.get():
@@ -680,16 +680,16 @@ def update_status(self, msg: UBXMessage):
if hasattr(msg, "CFG_PMP_UNIQUE_WORD"):
self._spartn_unqword.set(msg.CFG_PMP_UNIQUE_WORD)
self.__container.status_label = (f"{CFGPOLL} received", OKCOL)
- self._lbl_send.config(image=self._img_confirmed)
+ self._lbl_send["image"] = self._img_confirmed
elif msg.identity == "ACK-ACK":
- self._lbl_send.config(image=self._img_confirmed)
+ self._lbl_send["image"] = self._img_confirmed
self.__container.status_label = (CONFIGOK.format(CFGSET), OKCOL)
elif msg.identity == "ACK-NAK":
- self._lbl_send.config(image=self._img_warn)
+ self._lbl_send["image"] = self._img_warn
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}")
+ self._lbl_ebno["text"] = f"Eb/N0: {msg.ebno} dB"
+ self._lbl_fec["text"] = f"FEC Bits: {msg.fecBits}"
self.update_idletasks()
diff --git a/src/pygpsclient/spartn_mqtt_frame.py b/src/pygpsclient/spartn_mqtt_frame.py
index 9e59744a..b06a6174 100644
--- a/src/pygpsclient/spartn_mqtt_frame.py
+++ b/src/pygpsclient/spartn_mqtt_frame.py
@@ -35,6 +35,7 @@
Spinbox,
StringVar,
TclError,
+ Tk,
W,
ttk,
)
@@ -87,18 +88,17 @@ class SPARTNMQTTDialog(Frame):
SPARTNConfigDialog class.
"""
- def __init__(self, app: Frame, parent: Frame, *args, **kwargs):
+ def __init__(self, app: Tk, parent: Frame, *args, **kwargs):
"""
Constructor.
- :param Frame app: reference to main tkinter application
+ :param Tk app: reference to main tkinter application
:param Frame parent: reference to parent frame
:param args: optional args to pass to parent class (not currently used)
:param kwargs: optional kwargs to pass to parent class (not currently used)
"""
self.__app = app # Reference to main application class
- self.__master = self.__app.appmaster # Reference to root class (Tk)
self.__container = parent # container frame
super().__init__(parent.container, *args, **kwargs)
@@ -331,8 +331,8 @@ def _reset(self):
self.set_controls(DISCONNECTED)
if not HASCRYPTO:
- self._lbl_spartndecode.config(state=DISABLED)
- self._chk_spartndecode.config(state=DISABLED)
+ self._lbl_spartndecode["state"] = DISABLED
+ self._chk_spartndecode["state"] = DISABLED
def _reset_keypaths(self, clientid):
"""
@@ -354,13 +354,13 @@ def _on_source(self, var, index, mode): # pylint: disable=unused-argument
self.update()
cfg = self.__app.configuration
if self._mqtt_source.get() == MQTTLBANDMODE:
- self._chk_mqtt_iptopic.config(state=DISABLED)
- self._chk_mqtt_freqtopic.config(state=NORMAL)
+ self._chk_mqtt_iptopic["state"] = DISABLED
+ self._chk_mqtt_freqtopic["state"] = NORMAL
self._mqtt_iptopic.set(0)
self._mqtt_freqtopic.set(1)
else: # MQTTIPMODE
- self._chk_mqtt_iptopic.config(state=NORMAL)
- self._chk_mqtt_freqtopic.config(state=DISABLED)
+ self._chk_mqtt_iptopic["state"] = NORMAL
+ self._chk_mqtt_freqtopic["state"] = DISABLED
self._mqtt_iptopic.set(1)
self._mqtt_freqtopic.set(0)
cfg.set("mqttclientmode_n", self._mqtt_source.get())
@@ -467,7 +467,7 @@ def set_controls(self, status: int):
stat = NORMAL if status == CONNECTED_SPARTNIP else DISABLED
for wdg in (self._btn_disconnect,):
- wdg.config(state=stat)
+ wdg["state"] = stat
stat = DISABLED if status == CONNECTED_SPARTNIP else NORMAL
for wdg in (
self._ent_mqttserver,
@@ -485,10 +485,10 @@ def set_controls(self, status: int):
self._btn_opencrt,
self._btn_openpem,
):
- wdg.config(state=stat)
+ wdg["state"] = stat
stat = DISABLED if status == CONNECTED_SPARTNIP else READONLY
for wdg in (self._spn_mqttregion,):
- wdg.config(state=stat)
+ wdg["state"] = stat
if status == DISCONNECTED:
self._on_source(None, None, TRACEMODE_WRITE)
diff --git a/src/pygpsclient/spectrum_frame.py b/src/pygpsclient/spectrum_frame.py
index b891d12d..8ff48c8a 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, CENTER, EW, NSEW, NW, Checkbutton, Frame, IntVar, N, S, W
+from tkinter import ALL, CENTER, EW, NSEW, NW, Checkbutton, Frame, IntVar, N, S, Tk, W
from types import NoneType
from pyubx2 import UBXMessage
@@ -96,18 +96,17 @@ class SpectrumviewFrame(Frame):
Spectrumview frame class.
"""
- def __init__(self, app: Frame, parent: Frame, *args, **kwargs):
+ def __init__(self, app: Tk, parent: Frame, *args, **kwargs):
"""
Constructor.
- :param Frame app: reference to main tkinter application
+ :param Tk app: reference to main tkinter application
:param Frame parent: reference to parent frame
:param args: optional args to pass to Frame parent class
:param kwargs: optional kwargs to pass to Frame parent class
"""
self.__app = app # Reference to main application class
- self.__master = self.__app.appmaster # Reference to root class (Tk)
self.logger = logging.getLogger(__name__)
super().__init__(parent, *args, **kwargs)
diff --git a/src/pygpsclient/sqlite_handler.py b/src/pygpsclient/sqlite_handler.py
index 272d9034..a36cc8c0 100644
--- a/src/pygpsclient/sqlite_handler.py
+++ b/src/pygpsclient/sqlite_handler.py
@@ -103,12 +103,11 @@ def __init__(self, app):
"""
Constructor.
- :param Frame app: reference to main tkinter application
+ :param Tk app: reference to main tkinter application
"""
self.__app = app # Reference to main application class
- self.__master = self.__app.appmaster # Reference to root class (Tk)
self.logger = logging.getLogger(__name__)
self._db = None
@@ -132,7 +131,7 @@ def _create(
"""
try:
- self.__app.status_label = (DLGDBINIT.format(self._db), INFOCOL)
+ self.__app.set_status_label(DLGDBINIT.format(self._db), INFOCOL)
self.logger.debug("Spatial metadata initialisation in progress...")
self._connection.execute(SQLINIT)
self.logger.debug("Spatial metadata initialisation complete")
@@ -140,7 +139,7 @@ def _create(
self._cursor.executescript(SQLC1.format(table=tbname))
return SQLOK
except sqlite3.Error as err:
- self.__app.status_label = (DLGDBINITERR.format(err), ERRCOL)
+ self.__app.set_status_label(DLGDBINITERR.format(err), ERRCOL)
self.logger.debug(traceback.format_exc())
return SQLERR
@@ -184,18 +183,18 @@ def open(
if testing:
self._connection.close()
else:
- self.__app.status_label = (DLGDBOPEN.format(self._db), OKCOL)
+ self.__app.set_status_label(DLGDBOPEN.format(self._db), OKCOL)
return SQLOK
except AttributeError as err:
- self.__app.status_label = (DLGDBSQLERR.format(err), errcol)
+ self.__app.set_status_label(DLGDBSQLERR.format(err), errcol)
self.logger.debug(traceback.format_exc())
return NOEXT # extensions not supported
except sqlite3.OperationalError as err:
- self.__app.status_label = (DLGDBSQLERR.format(err), errcol)
+ self.__app.set_status_label(DLGDBSQLERR.format(err), errcol)
self.logger.debug(traceback.format_exc())
return NOMODS # no mod_spatial extension found
except sqlite3.Error as err:
- self.__app.status_label = (DLGDBSQLERR.format(err), errcol)
+ self.__app.set_status_label(DLGDBSQLERR.format(err), errcol)
self.logger.debug(traceback.format_exc())
return SQLERR # other sqlite error
@@ -260,7 +259,7 @@ def load_data(self, ignore_null: bool = True) -> int:
self.logger.debug(f"Executed SQL statement {sql}")
return SQLOK
except sqlite3.Error as err:
- self.__app.status_label = (DLGDBSQLERR.format(err), ERRCOL)
+ self.__app.set_status_label(DLGDBSQLERR.format(err), ERRCOL)
self.logger.debug(traceback.format_exc())
return SQLERR
diff --git a/src/pygpsclient/status_frame.py b/src/pygpsclient/status_frame.py
index d1d63900..e2a8027b 100644
--- a/src/pygpsclient/status_frame.py
+++ b/src/pygpsclient/status_frame.py
@@ -12,7 +12,7 @@
:license: BSD 3-Clause
"""
-from tkinter import EW, NS, VERTICAL, Frame, Label, W, ttk
+from tkinter import EW, NS, VERTICAL, Frame, Label, Tk, W, ttk
from pygpsclient.globals import BGCOL
@@ -22,20 +22,18 @@ class StatusFrame(Frame):
Status bar frame class.
"""
- def __init__(self, app: Frame, parent: Frame, *args, **kwargs):
+ def __init__(self, app: Tk, *args, **kwargs):
"""
Constructor
- :param Frame app: reference to main tkinter application
- :param Frame parent: reference to parent frame
+ :param Tk app: reference to main tkinter application
:param args: optional args to pass to Frame parent class
:param kwargs: optional kwargs to pass to Frame parent class
"""
self.__app = app # Reference to main application class
- self.__master = self.__app.appmaster # Reference to root class (Tk)
- super().__init__(parent, *args, **kwargs)
+ super().__init__(app, *args, **kwargs)
self.width, self.height = self.get_size()
self._body()
diff --git a/src/pygpsclient/stream_handler.py b/src/pygpsclient/stream_handler.py
index d88ca4c2..e44d07a9 100644
--- a/src/pygpsclient/stream_handler.py
+++ b/src/pygpsclient/stream_handler.py
@@ -44,7 +44,9 @@ class to read and parse incoming data from the receiver. It places
)
from threading import Event, Thread
from time import sleep
-from tkinter import Frame, Label, Tk
+from tkinter import Frame, Tk
+from types import NoneType
+from typing import Any
from certifi import where as findcacerts
from pygnssutils import (
@@ -56,12 +58,22 @@ class to read and parse incoming data from the receiver. It places
UNI_PROTOCOL,
GNSSError,
GNSSReader,
+ MQTTMessage,
)
-from pynmeagps import NMEAMessageError, NMEAParseError, NMEAStreamError
-from pyqgc import QGCMessageError, QGCParseError, QGCStreamError
-from pyrtcm import RTCMMessageError, RTCMParseError, RTCMStreamError
-from pysbf2 import SBFMessageError, SBFParseError, SBFStreamError
-from pyubx2 import ERR_LOG, UBXMessageError, UBXParseError, UBXStreamError
+
+try:
+ from pygnssutils.gnssreader import PARSE_FULL, PARSE_META, GNSSMessage
+except ImportError:
+ PARSE_FULL = 1
+ PARSE_META = 2
+ GNSSMessage = None
+from pynmeagps import NMEAMessage, NMEAMessageError, NMEAParseError, NMEAStreamError
+from pyqgc import QGCMessage, QGCMessageError, QGCParseError, QGCStreamError
+from pyrtcm import RTCMMessage, RTCMMessageError, RTCMParseError, RTCMStreamError
+from pysbf2 import SBFMessage, SBFMessageError, SBFParseError, SBFStreamError
+from pyspartn import SPARTNMessage
+from pyubx2 import ERR_LOG, UBXMessage, UBXMessageError, UBXParseError, UBXStreamError
+from pyunigps import UNIMessage, UNIMessageError, UNIParseError, UNIStreamError
try:
from pyubxutils import UBXSimulator
@@ -80,10 +92,15 @@ class to read and parse incoming data from the receiver. It places
CONNECTED_SIMULATOR,
CONNECTED_SOCKET,
DEFAULT_BUFSIZE,
- ERRCOL,
+ GNSS_PROTOCOL,
+ MQTT_PROTOCOL,
+ SPARTN_PROTOCOL,
+ STREAMDELAY,
TTY_PROTOCOL,
UBXSIMULATOR,
)
+from pygpsclient.strings import WARNING
+from pygpsclient.widget_state import VISIBLE, WDGCONSOLE
class StreamHandler:
@@ -95,12 +112,11 @@ def __init__(self, app):
"""
Constructor.
- :param Frame app: reference to main tkinter application
+ :param Tk app: reference to main tkinter application
"""
self.__app = app # Reference to main application class
- self.__master = self.__app.appmaster # Reference to root class (Tk)
self.logger = logging.getLogger(__name__)
self._stream_thread = None
@@ -119,10 +135,9 @@ def start(self, caller: Frame, settings: dict):
self._stream_thread = Thread(
target=self._read_thread,
args=(
- self.__master,
+ self.__app,
self._stopevent,
settings,
- caller.status_label, # for status update messages
),
daemon=True,
)
@@ -141,7 +156,6 @@ def _read_thread(
master: Tk,
stopevent: Event,
settings: dict,
- status: Label,
):
"""
THREADED PROCESS
@@ -182,7 +196,6 @@ def _read_thread(
) as stream:
if settings["protocol"] & TTY_PROTOCOL:
self._readlooptty(
- master,
stopevent,
stream,
settings,
@@ -190,7 +203,6 @@ def _read_thread(
)
else:
self._readloop(
- master,
stopevent,
stream,
settings,
@@ -201,7 +213,6 @@ def _read_thread(
in_filepath = settings["in_filepath"]
with open(in_filepath, "rb") as stream:
self._readloop(
- master,
stopevent,
stream,
settings,
@@ -238,7 +249,6 @@ def _read_thread(
if socktype == SOCK_DGRAM:
stream.send(b"") # send empty datagram to establish connection
self._readloop(
- master,
stopevent,
stream,
settings,
@@ -249,7 +259,6 @@ def _read_thread(
with UBXSimulator() as stream:
if settings["protocol"] & TTY_PROTOCOL:
self._readlooptty(
- master,
stopevent,
stream,
settings,
@@ -257,7 +266,6 @@ def _read_thread(
)
else:
self._readloop(
- master,
stopevent,
stream,
settings,
@@ -288,11 +296,10 @@ def _read_thread(
stopevent.set()
master.event_generate(settings["error_event"])
# use after(0) to avoid tkinter main thread contention
- status.after(0, status.config, {"text": f"{err} {fnam}", "fg": ERRCOL})
+ # status.after(0, status.config, {"text": f"{err} {fnam}", "fg": ERRCOL})
def _readloop(
self,
- master: Tk,
stopevent: Event,
stream: Serial | BufferedReader | socket,
settings: dict,
@@ -319,10 +326,14 @@ def _errorhandler(err: Exception):
"""
parsed_data = f"Error parsing data stream {err}"
- settings["inqueue"].put((raw_data, parsed_data))
- master.event_generate(settings["read_event"])
+ self._process_message(None, parsed_data)
conntype = settings["conntype"]
+ parsing = (
+ PARSE_META
+ if self.__app.configuration.get("metaprot_b") == 1
+ else PARSE_FULL
+ )
ubr = GNSSReader(
stream,
@@ -333,6 +344,7 @@ def _errorhandler(err: Exception):
| RTCM3_PROTOCOL
| UNI_PROTOCOL,
quitonerror=ERR_LOG,
+ parsing=parsing,
bufsize=DEFAULT_BUFSIZE,
msgmode=settings["msgmode"],
errorhandler=_errorhandler,
@@ -340,22 +352,13 @@ def _errorhandler(err: Exception):
raw_data = None
parsed_data = None
- lastread = datetime.now()
lastevent = datetime.now()
while not stopevent.is_set():
try:
- if conntype in (CONNECTED, CONNECTED_SOCKET) or (
- conntype == CONNECTED_FILE
- and datetime.now()
- > lastread
- + timedelta(
- milliseconds=self.__app.configuration.get("filedelay_n")
- )
- ):
+ if conntype in (CONNECTED, CONNECTED_SOCKET, CONNECTED_FILE):
raw_data, parsed_data = ubr.read()
if raw_data is not None:
- settings["inqueue"].put((raw_data, parsed_data))
- master.event_generate(settings["read_event"])
+ self._process_message(raw_data, parsed_data)
lastevent = datetime.now()
else: # timeout or eof
if conntype == CONNECTED_FILE:
@@ -365,9 +368,11 @@ def _errorhandler(err: Exception):
):
raise TimeoutError
if conntype == CONNECTED_FILE:
- lastread = datetime.now()
+ # sleep between file reads to keep UI responsive
+ sleep(self.__app.configuration.get("filedelay_n") / 1000)
# write any queued output data to serial stream
+ # e.g. receiver configuration commands or polls
if conntype in (CONNECTED, CONNECTED_SOCKET):
try:
while not settings["outqueue"].empty():
@@ -378,6 +383,8 @@ def _errorhandler(err: Exception):
except Empty:
pass
+ sleep(STREAMDELAY)
+
except (
UBXMessageError,
UBXParseError,
@@ -394,20 +401,16 @@ def _errorhandler(err: Exception):
QGCMessageError,
QGCParseError,
QGCStreamError,
- # UNIMessageError,
- # UNIParseError,
- # UNIStreamError,
+ UNIMessageError,
+ UNIParseError,
+ UNIStreamError,
GNSSError,
) as err:
_errorhandler(err)
continue
- # allow for any tkinter events e.g. dialogs
- self.__app.update_idletasks()
-
def _readlooptty(
self,
- master: Tk,
stopevent: Event,
stream: Serial,
settings: dict,
@@ -431,8 +434,7 @@ def _errorhandler(err: Exception):
"""
parsed_data = f"Error parsing data stream {err}"
- settings["inqueue"].put((raw_data, parsed_data))
- master.event_generate(settings["read_event"])
+ self._process_message(None, parsed_data)
raw_data = None
while not stopevent.is_set():
@@ -459,11 +461,86 @@ def _errorhandler(err: Exception):
# place ascii data on input queue
if raw_data != b"":
- settings["inqueue"].put(
- (raw_data, raw_data.decode(ASCII, errors=BSR))
- )
- master.event_generate(settings["read_event"])
+ self._process_message(raw_data, raw_data.decode(ASCII, errors=BSR))
+
+ sleep(STREAMDELAY)
except (ValueError, SerialException) as err:
_errorhandler(err)
continue
+
+ def _process_message(
+ self, raw_data: bytes | NoneType, parsed_data: Any, marker: str = ""
+ ):
+ """
+ THREADED PROCESS
+
+ NB: NO LONG-RUNNING PROCESSES OR DIRECT UPDATES TO TKINTER WIDGETS IN THIS FUNCTION!!!
+
+ Update the gnss_status dictionary with parsed attribute values.
+
+ :param bytes | NoneType raw_data: raw message data
+ :param object parsed data: NMEAMessage, UBXMessage or RTCMMessage
+ :param str marker: string prepended to console entries e.g. "NTRIP>>"
+ """
+
+ msgprot = 0
+ protfilter = self.__app.protocol_mask
+ parsing = self.__app.configuration.get("metaprot_b") == 0
+ tty = self.__app.configuration.get("ttyprot_b")
+ console = self.__app.widget_state.state[WDGCONSOLE][VISIBLE]
+
+ if parsing:
+ if isinstance(parsed_data, NMEAMessage) and protfilter & NMEA_PROTOCOL:
+ self.__app.nmea_handler.process_data(raw_data, parsed_data)
+ msgprot = NMEA_PROTOCOL
+ elif isinstance(parsed_data, UBXMessage) and protfilter & UBX_PROTOCOL:
+ self.__app.ubx_handler.process_data(raw_data, parsed_data)
+ msgprot = UBX_PROTOCOL
+ elif isinstance(parsed_data, RTCMMessage) and protfilter & RTCM3_PROTOCOL:
+ self.__app.rtcm_handler.process_data(raw_data, parsed_data)
+ msgprot = RTCM3_PROTOCOL
+ elif isinstance(parsed_data, SBFMessage) and protfilter & SBF_PROTOCOL:
+ self.__app.sbf_handler.process_data(raw_data, parsed_data)
+ msgprot = SBF_PROTOCOL
+ elif isinstance(parsed_data, QGCMessage) and protfilter & QGC_PROTOCOL:
+ self.__app.qgc_handler.process_data(raw_data, parsed_data)
+ msgprot = QGC_PROTOCOL
+ elif isinstance(parsed_data, UNIMessage) and protfilter & UNI_PROTOCOL:
+ self.__app.uni_handler.process_data(raw_data, parsed_data)
+ msgprot = UNI_PROTOCOL
+ elif (
+ isinstance(parsed_data, SPARTNMessage) and protfilter & SPARTN_PROTOCOL
+ ):
+ msgprot = SPARTN_PROTOCOL
+ elif isinstance(parsed_data, MQTTMessage):
+ msgprot = MQTT_PROTOCOL
+ elif isinstance(parsed_data, GNSSMessage):
+ msgprot = GNSS_PROTOCOL
+ elif isinstance(parsed_data, str):
+ if tty:
+ msgprot = TTY_PROTOCOL
+ self.__app.tty_handler.process_data(raw_data, parsed_data)
+ else:
+ msgprot = -1
+ marker = WARNING
+
+ # update consoledata if console is visible and protocol not filtered
+ if console and msgprot:
+ self.__app.console_outqueue.put((raw_data, parsed_data, marker))
+
+ # if socket server is running and has clients, output raw data to socket
+ if self.__app.server_status > 0:
+ self.__app.socket_outqueue.put(raw_data)
+
+ # update log file if enabled
+ if self.__app.configuration.get("datalog_b"):
+ self.__app.file_handler.write_logfile(raw_data, parsed_data)
+
+ # update GPX track file if enabled
+ if self.__app.configuration.get("recordtrack_b"):
+ self.__app.file_handler.update_gpx_track()
+
+ # update database if enabled
+ if self.__app.configuration.get("database_b"):
+ self.__app.sqlite_handler.load_data()
diff --git a/src/pygpsclient/strings.py b/src/pygpsclient/strings.py
index af997905..68b07b7d 100644
--- a/src/pygpsclient/strings.py
+++ b/src/pygpsclient/strings.py
@@ -49,7 +49,7 @@
HWREPOLL = "Hardware information re-polled..."
HWRESET = "Hardware information reset"
INACTIVE_TIMEOUT = "Inactivity timeout"
-KILLSWITCH = "Running threads terminated by user"
+KILLSWITCH = "Running processes terminated by user"
LOADCONFIGBAD = "Configuration not loaded {} {}. Using defaults"
LOADCONFIGNK = "Unrecognised configuration setting '{}: {}'"
LOADCONFIGNONE = "Configuration file not found {}. Using defaults"
@@ -136,7 +136,7 @@
LBLDEGFORMAT = "Units"
LBLDISNMEA = "Disable NMEA"
LBLDURATIONS = "Duration (s)"
-LBLFILEDELAY = "File Read Delay"
+LBLFILEDELAY = "File Delay"
LBLGGAFIXED = "Fixed Reference"
LBLGGALIVE = "Receiver"
LBLJSONLOAD = "Load Keys From JSON"
@@ -170,6 +170,7 @@
LBLRINEXRCVR = "Receiver Number / Name / Version:"
LBLRINEXSTARTTIME = "Approximate Start Time of RTCM 3 Data:"
LBLRINEXSTATION = "Station Metadata:"
+LBLRINEXSVCODES = "Satellite (PRN) Codes (comma-separated, blank for ALL):"
LBLRINEXTYPES = "RINEX Output File Types and Data Sources:"
LBLRINEXVER = "RINEX Version:"
LBLSERVERCONFIG = "Server\nConfig"
@@ -194,6 +195,7 @@
LBLTRACKRECORD = "GPX Track"
LBLTTYCONFIG = "TTY\nConfig"
LBLUBXCONFIG = "UBX\nConfig"
+LBLUBXLEGACYCONFIG = "UBX\nLegacy"
LBLUBXPRESET = "Preset UBX Configuration Commands"
LBLUDPORT = "USER-DEFINED PORT"
@@ -234,6 +236,7 @@
DLGTSPARTN = "SPARTN Configuration"
DLGTTTY = "TTY Configuration"
DLGTUBX = "UBX Configuration"
+DLGTUBXLEGACY = "UBX Legacy Configuration"
DLGWAITAZI = "Waiting for ELV/AZI data"
DLGWAITCNO = "Waiting for CNO data"
DLGWAITATTITUDE = "Waiting for Attitude data"
diff --git a/src/pygpsclient/sysmon_frame.py b/src/pygpsclient/sysmon_frame.py
index 209901db..55ac6d71 100644
--- a/src/pygpsclient/sysmon_frame.py
+++ b/src/pygpsclient/sysmon_frame.py
@@ -14,7 +14,7 @@
:license: BSD 3-Clause
"""
-from tkinter import EW, NSEW, NW, Canvas, E, Frame, IntVar, Radiobutton, W
+from tkinter import EW, NSEW, NW, Canvas, E, Frame, IntVar, Radiobutton, Tk, W
from pyubx2 import BOOTTYPE, UBXMessage
@@ -36,13 +36,21 @@
MAXLINES = 19
MAXTEMP = 100 # °C
PORTIDS = {
- 0x0000: "I2C", # 0 I2C
- 0x0100: "UART1", # 256 UART1
- 0x0101: "INT1", # 257 inter-cpu connect
- 0x0200: "INT2", # 512 inter-cpu connect
- 0x0201: "UART2", # 513 UART2
- 0x0300: "USB", # 768 USB
- 0x0400: "SPI", # 1024 SPI
+ ("F9", 0x0000): "I2C", # 0 I2C
+ ("F9", 0x0100): "UART1", # 256 UART1
+ ("F9", 0x0101): "INT1", # 257 inter-cpu connect
+ ("F9", 0x0200): "INT2", # 512 inter-cpu connect
+ ("F9", 0x0201): "UART2", # 513 UART2
+ ("F9", 0x0300): "USB", # 768 USB
+ ("F9", 0x0400): "SPI", # 1024 SPI
+ ("X20", 0x0000): "I2C", # 0 I2C
+ ("X20", 0x0100): "UART1", # 256 UART1
+ ("X20", 0x0101): "INT1", # 257 inter-cpu connect
+ ("X20", 0x0200): "UART2", # 512 UART2
+ ("X20", 0x0201): "INT2", # 513 inter-cpu connect
+ ("X20", 0x0300): "USB", # 768 USB
+ ("X20", 0x0400): "SPI", # 1024 SPI
+ ("X20", 0x0500): "INT3", # 1280 inter-cpu connect
}
SPACING = 2
@@ -52,18 +60,17 @@ class SysmonFrame(Frame):
SysmonFrame class.
"""
- def __init__(self, app: Frame, parent: Frame, *args, **kwargs):
+ def __init__(self, app: Tk, parent: Frame, *args, **kwargs):
"""
Constructor.
- :param Frame app: reference to main tkinter application
+ :param Tk app: reference to main tkinter application
:param Frame parent: reference to parent frame
:param args: optional args to pass to Frame parent class
:param kwargs: optional kwargs to pass to Frame parent class
"""
self.__app = app # Reference to main application class
- self.__master = self.__app.appmaster # Reference to root class (Tk)
super().__init__(parent, *args, **kwargs)
@@ -249,6 +256,8 @@ def update_frame(self):
font=self._font,
tags=TAG_DATA,
)
+
+ self.update_idletasks()
except KeyError: # invalid sysmon-data or comms-data
self.init_chart()
@@ -323,7 +332,10 @@ def _chart_io(self, xoffset: int, y: int, port: int, pdata: tuple):
txf = "d" if txbu == "" else ".02f"
rxb, rxbu = bytes2unit(pdata[7 if mod else 6])
rxf = "d" if rxbu == "" else ".02f"
- txt = f"{PORTIDS.get(port, NA)} → {txb:{txf}} {txbu} ← {rxb:{rxf}} {rxbu}:"
+ hw = (
+ "X20" if "X20" in self.__app.gnss_status.version_data["hwversion"] else "F9"
+ )
+ txt = f"{PORTIDS.get((hw,port), NA)} → {txb:{txf}} {txbu} ← {rxb:{rxf}} {rxbu}:"
self._canvas.create_text( # port
x,
y,
diff --git a/src/pygpsclient/toplevel_dialog.py b/src/pygpsclient/toplevel_dialog.py
index 4181b0e0..c18c7eb1 100644
--- a/src/pygpsclient/toplevel_dialog.py
+++ b/src/pygpsclient/toplevel_dialog.py
@@ -27,6 +27,7 @@
Frame,
Label,
TclError,
+ Tk,
Toplevel,
W,
)
@@ -66,16 +67,16 @@ class ToplevelDialog(Toplevel):
ToplevelDialog class.
"""
- def __init__(self, app, dlgname: str, *args, **kwargs):
+ def __init__(self, app: Tk, dlgname: str, *args, **kwargs):
"""
Constructor.
- :param Frame app: reference to main tkinter application
+ :param Tk app: reference to main tkinter application
:param str dlgname: dialog name
"""
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.width, self.height = 300, 300 # initial, updated in finalise()
@@ -90,7 +91,7 @@ def __init__(self, app, dlgname: str, *args, **kwargs):
"transient", self.__app.configuration.get("transient_dialog_b")
)
- super().__init__(self.__master, *args, **kwargs)
+ super().__init__(self.__app, *args, **kwargs)
if transient:
self.transient(self.__app)
@@ -175,14 +176,14 @@ def _finalise(self):
self._frm_container.update_idletasks()
fh = self._frm_container.winfo_height()
fw = self._frm_container.winfo_width()
- lowres, (sh, sw) = check_lowres(self.__master, (fh, fw))
+ lowres, (sh, sw) = check_lowres(self.__app, (fh, fw))
if lowres:
- self._can_container.config(
- height=min(int(sh * 0.75), fh), width=min(int(sw * 0.75), fw)
- )
+ self._can_container["height"] = min(int(sh * 0.75), fh)
+ self._can_container["width"] = min(int(sw * 0.75), fw)
self.resizable(True, True)
else:
- self._can_container.config(height=fh, width=fw)
+ self._can_container["height"] = fh
+ self._can_container["width"] = fw
self._can_container.show_scroll(False)
self.resizable(self._resizable, self._resizable)
@@ -219,7 +220,7 @@ def get_size(self) -> tuple[int, int]:
:rtype: tuple[int,int]
"""
- self.__master.update_idletasks() # Make sure we know about any resizing
+ self.__app.update_idletasks() # Make sure we know about any resizing
return self.winfo_width(), self.winfo_height()
@property
@@ -233,38 +234,21 @@ 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]):
+ def set_status_label(self, msg: str, col: str = INFOCOL):
"""
Setter for status_label.
- :param self: Description
- :param tuple | str message: (message, color))
+ :param str msg: message
+ :param str col: color
"""
try:
- if isinstance(message, tuple):
- message, color = message
- else:
- color = INFOCOL
-
# truncate very long messages
- if len(message) > 100:
- message = "..." + message[-100:]
+ if len(msg) > 100:
+ msg = "..." + msg[-100:]
- self.status_label.config(text=message, fg=color)
- self.status_label.update()
+ self._lbl_status["text"] = msg
+ self._lbl_status["fg"] = col
+ self._lbl_status.update()
except TclError:
pass
diff --git a/src/pygpsclient/tty_handler.py b/src/pygpsclient/tty_handler.py
index ed96fe56..eea2cf6d 100644
--- a/src/pygpsclient/tty_handler.py
+++ b/src/pygpsclient/tty_handler.py
@@ -33,11 +33,10 @@ def __init__(self, app):
"""
Constructor.
- :param Frame app: reference to main tkinter application
+ :param Tk app: reference to main tkinter application
"""
self.__app = app # Reference to main application class
- self.__master = self.__app.appmaster # Reference to root class (Tk)
self.logger = logging.getLogger(__name__)
def process_data(self, raw_data: bytes, parsed_data: str):
diff --git a/src/pygpsclient/tty_preset_dialog.py b/src/pygpsclient/tty_preset_dialog.py
index 4e4b240e..8834117a 100644
--- a/src/pygpsclient/tty_preset_dialog.py
+++ b/src/pygpsclient/tty_preset_dialog.py
@@ -29,6 +29,7 @@
S,
Scrollbar,
StringVar,
+ Tk,
W,
ttk,
)
@@ -69,17 +70,16 @@ class TTYPresetDialog(ToplevelDialog):
TTY Preset and User-defined configuration command dialog.
"""
- def __init__(self, app, *args, **kwargs): # pylint: disable=unused-argument
+ def __init__(self, app: Tk, *args, **kwargs): # pylint: disable=unused-argument
"""
Constructor.
- :param Frame app: reference to main tkinter application
+ :param Tk app: reference to main tkinter application
:param args: optional args to pass to parent class (not currently used)
:param kwargs: optional kwargs to pass to Frame parent class
"""
self.__app = app
- self.__master = self.__app.appmaster # Reference to root class (Tk)
super().__init__(app, DLGTTTY)
self._confirm = False
@@ -139,10 +139,10 @@ def _body(self):
)
self._scr_presetv = Scrollbar(self._frm_body, orient=VERTICAL)
self._scr_preseth = Scrollbar(self._frm_body, orient=HORIZONTAL)
- self._lbx_preset.config(yscrollcommand=self._scr_presetv.set)
- self._lbx_preset.config(xscrollcommand=self._scr_preseth.set)
- self._scr_presetv.config(command=self._lbx_preset.yview)
- self._scr_preseth.config(command=self._lbx_preset.xview)
+ self._lbx_preset["yscrollcommand"] = self._scr_presetv.set
+ self._lbx_preset["xscrollcommand"] = self._scr_preseth.set
+ self._scr_presetv["command"] = self._lbx_preset.yview
+ self._scr_preseth["command"] = self._lbx_preset.xview
self._lbl_send_command = Label(self._frm_body, image=self.img_none)
self._btn_send_command = Button(
self._frm_body,
@@ -159,13 +159,13 @@ def _do_layout(self):
self.frm_device_info.grid(column=0, row=0, sticky=EW)
self._frm_body.grid(column=0, row=1, sticky=NSEW)
- self._lbl_command.grid(column=0, row=0, sticky=W)
- self._ent_command.grid(column=1, row=0, columnspan=4, sticky=EW)
- self._chk_crlf.grid(column=0, row=1, sticky=W)
- self._chk_echo.grid(column=1, row=1, sticky=W)
- self._chk_delay.grid(column=2, row=1, sticky=W)
- ttk.Separator(self._frm_body).grid(column=0, row=2, columnspan=5, sticky=EW)
- self._lbl_presets.grid(column=0, row=3, columnspan=4, sticky=EW)
+ self._lbl_presets.grid(column=0, row=0, columnspan=4, sticky=EW)
+ self._lbl_command.grid(column=0, row=1, sticky=W)
+ self._ent_command.grid(column=1, row=1, columnspan=4, sticky=EW)
+ self._chk_crlf.grid(column=0, row=2, sticky=W)
+ self._chk_echo.grid(column=1, row=2, sticky=W)
+ self._chk_delay.grid(column=2, row=2, sticky=W)
+ ttk.Separator(self._frm_body).grid(column=0, row=3, columnspan=5, sticky=EW)
self._lbx_preset.grid(
column=0,
row=4,
@@ -217,7 +217,7 @@ def _on_update_command(self, var, index, mode): # pylint: disable=unused-argume
Command has been updated.
"""
- self._lbl_send_command.config(image=self.img_none)
+ self._lbl_send_command["image"] = self.img_none
def _on_update_settings(self, var, index, mode): # pylint: disable=unused-argument
"""
@@ -234,13 +234,13 @@ def _on_select_preset(self, *args, **kwargs): # pylint: disable=unused-argument
"""
try:
- self.status_label = ("", INFOCOL)
+ self.set_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.status_label = ("Invalid preset format", ERRCOL)
+ self.set_status_label("Invalid preset format", ERRCOL)
def _on_send_command(self, *args, **kwargs): # pylint: disable=unused-argument
"""
@@ -248,7 +248,7 @@ def _on_send_command(self, *args, **kwargs): # pylint: disable=unused-argument
"""
if not self._ent_command.validate(VALNONBLANK):
- self.status_label = ("Invalid command format", ERRCOL)
+ self.set_status_label("Invalid command format", ERRCOL)
return
try:
@@ -262,17 +262,17 @@ def _on_send_command(self, *args, **kwargs): # pylint: disable=unused-argument
self._parse_command(self._command.get())
status = CONFIRMED
if status == CONFIRMED:
- self._lbl_send_command.config(image=self.img_pending)
- self.status_label = "Command(s) sent"
+ self._lbl_send_command["image"] = self.img_pending
+ self.set_status_label("Command(s) sent")
elif status == CANCELLED:
- self.status_label = "Command(s) cancelled"
+ self.set_status_label("Command(s) cancelled")
elif status == NOMINAL:
- self.status_label = "Command(s) sent, no results"
+ self.set_status_label("Command(s) sent, no results")
self._confirm = False
except Exception as err: # pylint: disable=broad-except
- self.status_label = (f"Error {err}", ERRCOL)
- self._lbl_send_command.config(image=self.img_warn)
+ self.set_status_label(f"Error {err}", ERRCOL)
+ self._lbl_send_command["image"] = self.img_warn
def _parse_command(self, command: str):
"""
@@ -299,8 +299,8 @@ def _parse_command(self, command: str):
cmds.append(cmd)
self.__app.send_to_device(cmds, interval=self._delay.get() * CMDPAUSE)
except Exception as err: # pylint: disable=broad-except
- self.status_label = (f"Error {err}", ERRCOL)
- self._lbl_send_command.config(image=self.img_warn)
+ self.set_status_label(f"Error {err}", ERRCOL)
+ self._lbl_send_command["image"] = self.img_warn
def _record_command(self, msg: bytes):
"""
@@ -322,11 +322,11 @@ def update_status(self, msg: bytes):
msgstr = msg.decode(ASCII, errors=BSR).upper()
for ack in TTYOK:
if ack in msgstr:
- self._lbl_send_command.config(image=self.img_confirmed)
- self.status_label = ("Command(s) acknowledged", OKCOL)
+ self._lbl_send_command["image"] = self.img_confirmed
+ self.set_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.status_label = ("Command(s) rejected", ERRCOL)
+ self._lbl_send_command["image"] = self.img_warn
+ self.set_status_label("Command(s) rejected", ERRCOL)
break
diff --git a/src/pygpsclient/ubx_cfgval_frame.py b/src/pygpsclient/ubx_cfgval_frame.py
index 5e2a8a7d..1e90bb1e 100644
--- a/src/pygpsclient/ubx_cfgval_frame.py
+++ b/src/pygpsclient/ubx_cfgval_frame.py
@@ -17,6 +17,7 @@
HORIZONTAL,
LEFT,
NORMAL,
+ NSEW,
VERTICAL,
Button,
E,
@@ -32,6 +33,7 @@
Spinbox,
StringVar,
TclError,
+ Tk,
W,
)
@@ -78,18 +80,17 @@ class UBX_CFGVAL_Frame(Frame):
UBX CFG-VAL configuration command panel.
"""
- def __init__(self, app: Frame, parent: Frame, *args, **kwargs):
+ def __init__(self, app: Tk, parent: Frame, *args, **kwargs):
"""
Constructor.
- :param Frame app: reference to main tkinter application
+ :param Tk app: reference to main tkinter application
:param Frame parent: reference to parent frame (config-dialog)
:param args: optional args to pass to Frame parent class
:param kwargs: optional kwargs to pass to Frame parent class
"""
self.__app = app # Reference to main application class
- self.__master = self.__app.appmaster # Reference to root class (Tk)
self.__container = parent
super().__init__(parent.container, *args, **kwargs)
@@ -132,10 +133,10 @@ def _body(self):
)
self._scr_catv = Scrollbar(self, orient=VERTICAL)
self._scr_cath = Scrollbar(self, orient=HORIZONTAL)
- self._lbx_cat.config(yscrollcommand=self._scr_catv.set)
- self._lbx_cat.config(xscrollcommand=self._scr_cath.set)
- self._scr_catv.config(command=self._lbx_cat.yview)
- self._scr_cath.config(command=self._lbx_cat.xview)
+ self._lbx_cat["yscrollcommand"] = self._scr_catv.set
+ self._lbx_cat["xscrollcommand"] = self._scr_cath.set
+ self._scr_catv["command"] = self._lbx_cat.yview
+ self._scr_cath["command"] = self._lbx_cat.xview
self._lbl_parm = Label(self, text="Keyname", anchor=W)
self._lbx_parm = Listbox(
self,
@@ -147,10 +148,10 @@ def _body(self):
)
self._scr_parmv = Scrollbar(self, orient=VERTICAL)
self._scr_parmh = Scrollbar(self, orient=HORIZONTAL)
- self._lbx_parm.config(yscrollcommand=self._scr_parmv.set)
- self._lbx_parm.config(xscrollcommand=self._scr_parmh.set)
- self._scr_parmv.config(command=self._lbx_parm.yview)
- self._scr_parmh.config(command=self._lbx_parm.xview)
+ self._lbx_parm["yscrollcommand"] = self._scr_parmv.set
+ self._lbx_parm["xscrollcommand"] = self._scr_parmh.set
+ self._scr_parmv["command"] = self._lbx_parm.yview
+ self._scr_parmh["command"] = self._lbx_parm.xview
self._rad_cfgset = Radiobutton(
self, text="CFG-VALSET", variable=self._cfgmode, value=0
@@ -210,11 +211,11 @@ def _do_layout(self):
self._lbl_configdb.grid(column=0, row=0, columnspan=5, sticky=EW)
self._lbl_cat.grid(column=0, row=1, sticky=EW)
- self._lbx_cat.grid(column=0, row=2, rowspan=10, sticky=EW)
+ self._lbx_cat.grid(column=0, row=2, rowspan=10, sticky=NSEW)
self._scr_catv.grid(column=0, row=2, rowspan=10, sticky=(N, S, E))
self._scr_cath.grid(column=0, row=12, sticky=EW)
self._lbl_parm.grid(column=1, row=1, columnspan=4, sticky=EW)
- self._lbx_parm.grid(column=1, row=2, columnspan=4, rowspan=10, sticky=EW)
+ self._lbx_parm.grid(column=1, row=2, columnspan=4, rowspan=10, sticky=NSEW)
self._scr_parmv.grid(column=4, row=2, rowspan=10, sticky=(N, S, E))
self._scr_parmh.grid(column=1, row=12, columnspan=4, sticky=EW)
self._rad_cfgget.grid(column=0, row=13, sticky=W)
@@ -235,7 +236,6 @@ def _do_layout(self):
self._lbl_send_command.grid(
column=4, row=16, rowspan=2, ipadx=3, ipady=3, sticky=E
)
- self.option_add("*Font", self.__app.font_sm)
def _attach_events(self):
"""
@@ -263,7 +263,7 @@ def reset(self):
for i, cat in enumerate(cdb_cats):
self._lbx_cat.insert(i, cat)
self._cfgmode.set(2)
- self._lbl_send_command.config(image=self._img_blank)
+ self._lbl_send_command["image"] = self._img_blank
def _on_select_mode(self, *args, **kwargs): # pylint: disable=unused-argument
"""
@@ -271,10 +271,10 @@ def _on_select_mode(self, *args, **kwargs): # pylint: disable=unused-argument
"""
if self._cfgmode.get() == VALSET:
- self._ent_val.config(state=NORMAL)
+ self._ent_val["state"] = NORMAL
else:
- self._ent_val.config(state=READONLY)
- self._lbl_send_command.config(image=self._img_blank)
+ self._ent_val["state"] = READONLY
+ self._lbl_send_command["image"] = self._img_blank
def _on_select_cat(self, *args, **kwargs): # pylint: disable=unused-argument
"""
@@ -293,7 +293,7 @@ def _on_select_cat(self, *args, **kwargs): # pylint: disable=unused-argument
self._lbx_parm.insert(idx, keyname)
idx += 1
self._cfgval.set("")
- self._lbl_send_command.config(image=self._img_blank)
+ self._lbl_send_command["image"] = self._img_blank
def _on_select_parm(self, *args, **kwargs): # pylint: disable=unused-argument
"""
@@ -308,7 +308,7 @@ def _on_select_parm(self, *args, **kwargs): # pylint: disable=unused-argument
self._cfgkeyid.set(hex(keyid))
self._cfgatt.set(att)
self._cfgval.set("")
- self._lbl_send_command.config(image=self._img_blank)
+ self._lbl_send_command["image"] = self._img_blank
except TclError:
pass
@@ -317,7 +317,7 @@ def _on_send_config(self, *args, **kwargs): # pylint: disable=unused-argument
Config interface send button has been clicked.
"""
- self._lbl_send_command.config(image=self._img_blank)
+ self._lbl_send_command["image"] = self._img_blank
if self._cfgval_keyname is not None:
if self._cfgmode.get() == VALSET:
self._do_valset()
@@ -388,12 +388,12 @@ def _do_valset(self):
if valid:
msg = UBXMessage.config_set(layers, transaction, cfgData)
self.__container.send_command(msg)
- self._lbl_send_command.config(image=self._img_pending)
+ self._lbl_send_command["image"] = self._img_pending
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)
+ self._lbl_send_command["image"] = self._img_warn
typ = ATTDICT[att]
self.__container.status_label = (
(
@@ -423,7 +423,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._lbl_send_command["image"] = self._img_pending
self.__container.status_label = "CFG-VALDEL SET message sent"
for msgid in ("ACK-ACK", "ACK-NAK"):
self.__container.set_pending(msgid, UBX_CFGVAL)
@@ -448,7 +448,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._lbl_send_command["image"] = self._img_pending
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)
@@ -461,7 +461,7 @@ def update_status(self, msg: UBXMessage): # pylint: disable=unused-argument
"""
if msg.identity == "CFG-VALGET":
- self._lbl_send_command.config(image=self._img_confirmed)
+ self._lbl_send_command["image"] = self._img_confirmed
val = getattr(msg, self._cfgval_keyname, None)
if val is not None:
if isinstance(val, bytes):
@@ -472,9 +472,9 @@ def update_status(self, msg: UBXMessage): # pylint: disable=unused-argument
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._lbl_send_command["image"] = self._img_confirmed
self.__container.status_label = ("CFG-VAL command acknowledged", OKCOL)
elif msg.identity == "ACK-NAK":
- self._lbl_send_command.config(image=self._img_warn)
+ self._lbl_send_command["image"] = self._img_warn
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 0c386a5f..bfd9b837 100644
--- a/src/pygpsclient/ubx_config_dialog.py
+++ b/src/pygpsclient/ubx_config_dialog.py
@@ -27,16 +27,11 @@
from pyubx2 import SET, UBXMessage
-from pygpsclient.dynamic_config_frame import Dynamic_Config_Frame
from pygpsclient.globals import (
CONNECTED,
CONNECTED_SIMULATOR,
CONNECTED_SOCKET,
ERRCOL,
- UBX_CFGMSG,
- UBX_CFGOTHER,
- UBX_CFGPRT,
- UBX_CFGRATE,
UBX_CFGVAL,
UBX_MONHW,
UBX_MONRF,
@@ -47,10 +42,7 @@
from pygpsclient.strings import DLGTUBX
from pygpsclient.toplevel_dialog import ToplevelDialog
from pygpsclient.ubx_cfgval_frame import UBX_CFGVAL_Frame
-from pygpsclient.ubx_msgrate_frame import UBX_MSGRATE_Frame
-from pygpsclient.ubx_port_frame import UBX_PORT_Frame
from pygpsclient.ubx_preset_frame import UBX_PRESET_Frame
-from pygpsclient.ubx_solrate_frame import UBX_RATE_Frame
class UBXConfigDialog(ToplevelDialog):
@@ -62,7 +54,7 @@ def __init__(self, app, *args, **kwargs): # pylint: disable=unused-argument
"""
Constructor.
- :param Frame app: reference to main tkinter application
+ :param Tk app: reference to main tkinter application
:param args: optional args to pass to parent class (not currently used)
:param kwargs: optional kwargs to pass to parent class (not currently used)
"""
@@ -90,18 +82,6 @@ def _body(self):
self.frm_device_info = Hardware_Info_Frame(
self.__app, self, protocol="UBX", borderwidth=2, relief="groove"
)
- self._frm_config_port = UBX_PORT_Frame(
- self.__app, self, borderwidth=2, relief="groove"
- )
- self._frm_config_rate = UBX_RATE_Frame(
- self.__app, self, borderwidth=2, relief="groove"
- )
- self._frm_config_msg = UBX_MSGRATE_Frame(
- self.__app, self, borderwidth=2, relief="groove"
- )
- self._frm_config_dynamic = Dynamic_Config_Frame(
- self.__app, self, protocol="UBX", borderwidth=2, relief="groove"
- )
self._frm_configdb = UBX_CFGVAL_Frame(
self.__app, self, borderwidth=2, relief="groove"
)
@@ -118,42 +98,31 @@ def _do_layout(self):
"""
self.frm_device_info.grid(column=0, row=0, columnspan=3, sticky=NSEW)
- self._frm_config_port.grid(column=0, row=1, sticky=NSEW)
- self._frm_config_rate.grid(column=0, row=2, sticky=NSEW)
- self._frm_config_msg.grid(column=0, row=3, sticky=NSEW)
- self._frm_configdb.grid(column=1, row=1, rowspan=2, sticky=NSEW)
- self._frm_preset.grid(column=1, row=3, rowspan=2, sticky=NSEW)
- self._frm_config_dynamic.grid(column=2, row=1, rowspan=4, sticky=NSEW)
-
- for col in range(0, 3):
+ self._frm_configdb.grid(column=0, row=1, rowspan=1, sticky=NSEW)
+ self._frm_preset.grid(column=1, row=1, rowspan=1, sticky=NSEW)
+
+ for col in range(0, 2):
self.container.grid_columnconfigure(col, weight=1)
- for row in range(0, 4):
+ for row in range(1, 2):
self.container.grid_rowconfigure(row, weight=1)
- colsp, rowsp = self._frm_preset.grid_size()
- for col in range(colsp - 2):
- self._frm_preset.grid_columnconfigure(col, weight=1)
+ self._frm_configdb.grid_columnconfigure(0, weight=1)
+ self._frm_configdb.grid_columnconfigure(1, weight=2)
+ self._frm_configdb.grid_rowconfigure(2, weight=1)
+ self._frm_preset.grid_columnconfigure(0, weight=1)
self._frm_preset.grid_rowconfigure(2, weight=1)
- colsp, rowsp = self._frm_config_dynamic.grid_size()
- for col in range(colsp):
- self._frm_config_dynamic.grid_columnconfigure(col, weight=1)
- for row in range(1, rowsp):
- self._frm_config_dynamic.grid_rowconfigure(row, weight=1)
def _reset(self):
"""
Reset configuration widgets.
"""
- self._frm_config_rate.reset()
- self._frm_config_port.reset()
- self._frm_config_dynamic.reset()
self.frm_device_info.reset()
if self.__app.conn_status not in (
CONNECTED,
CONNECTED_SOCKET,
CONNECTED_SIMULATOR,
):
- self.status_label = ("Device not connected", ERRCOL)
+ self.set_status_label("Device not connected", ERRCOL)
def _attach_events(self):
"""
@@ -186,18 +155,10 @@ def update_pending(self, msg: UBXMessage):
if ubxfrm is not None:
if ubxfrm in (UBX_MONVER, UBX_MONHW, UBX_MONRF):
self.frm_device_info.reset()
- elif ubxfrm == UBX_CFGPRT:
- self._frm_config_port.update_status(msg)
- elif ubxfrm == UBX_CFGRATE:
- self._frm_config_rate.update_status(msg)
- elif ubxfrm == UBX_CFGMSG:
- self._frm_config_msg.update_status(msg)
elif ubxfrm == UBX_CFGVAL:
self._frm_configdb.update_status(msg)
elif ubxfrm == UBX_PRESET:
self._frm_preset.update_status(msg)
- elif ubxfrm == UBX_CFGOTHER:
- self._frm_config_dynamic.update_status(msg)
# reset all confirmation flags for this frame
for msgid in (msg.identity, "ACK-ACK", "ACK-NAK"):
diff --git a/src/pygpsclient/ubx_handler.py b/src/pygpsclient/ubx_handler.py
index 66dae68b..4a7fc541 100644
--- a/src/pygpsclient/ubx_handler.py
+++ b/src/pygpsclient/ubx_handler.py
@@ -22,7 +22,7 @@
from pygpsclient.globals import BSR, GLONASS_NMEA, UTF8
from pygpsclient.helpers import corrage2int, fix2desc, hdg2yaw, ned2vector, svid2gnssid
-from pygpsclient.strings import DLGTSERVER, DLGTSPARTN, DLGTUBX, NA
+from pygpsclient.strings import DLGTSERVER, DLGTSPARTN, DLGTUBX, DLGTUBXLEGACY, NA
from pygpsclient.widget_state import VISIBLE, WDGSIGNALS, WDGSPECTRUM, WDGSYSMON
UBXMODELS = {
@@ -43,11 +43,10 @@ def __init__(self, app):
"""
Constructor.
- :param Frame app: reference to main tkinter application
+ :param Tk app: reference to main tkinter application
"""
self.__app = app # Reference to main application class
- self.__master = self.__app.appmaster # Reference to root class (Tk)
self.logger = logging.getLogger(__name__)
self._cdb = 0
@@ -66,7 +65,9 @@ def process_data(self, raw_data: bytes, parsed_data: object):
return
if self.__app.gnss_status.version_data["hwversion"] == NA:
self.__app.gnss_status.version_data["hwversion"] = "u-blox"
- self.__app.device_label = self.__app.gnss_status.version_data["hwversion"]
+ self.__app.set_device_label(
+ self.__app.gnss_status.version_data["hwversion"]
+ )
# self.logger.debug(f"data received {parsed_data.identity}")
if parsed_data.identity[0:3] in ("ACK", "CFG"):
self._process_ACK(parsed_data)
@@ -86,8 +87,10 @@ def process_data(self, raw_data: bytes, parsed_data: object):
self._process_NAV_PVT(parsed_data)
elif parsed_data.identity in ("NAV-PVAT", "NAV2-PVAT"):
self._process_NAV_PVAT(parsed_data)
- elif parsed_data.identity == "NAV-RELPOSNED":
+ elif parsed_data.identity in ("NAV-RELPOSNED",):
self._process_NAV_RELPOSNED(parsed_data)
+ elif parsed_data.identity in ("NAV-DAHEADING",):
+ self._process_NAV_DAHEADING(parsed_data)
elif parsed_data.identity in ("NAV-SAT", "NAV2-SAT"):
self._process_NAV_SAT(parsed_data)
elif parsed_data.identity in ("NAV-SIG", "NAV2-SIG"):
@@ -126,6 +129,8 @@ def _process_ACK(self, msg: UBXMessage):
if self.__app.dialog(DLGTUBX) is not None:
self.__app.dialog(DLGTUBX).update_pending(msg)
+ if self.__app.dialog(DLGTUBXLEGACY) is not None:
+ self.__app.dialog(DLGTUBXLEGACY).update_pending(msg)
# if SPARTN config dialog is open, send CFG & ACKs there
if self.__app.dialog(DLGTSPARTN) is not None:
@@ -201,7 +206,7 @@ def _process_MONVER(self, msg: UBXMessage):
if self.__app.dialog(DLGTUBX) is not None:
self.__app.dialog(DLGTUBX).update_pending(msg)
# device = self.__app.gnss_status.version_data["hwversion"].rsplit(" ", 1)[0]
- self.__app.device_label = self.__app.gnss_status.version_data["hwversion"]
+ self.__app.set_device_label(self.__app.gnss_status.version_data["hwversion"])
def _process_MON_SYS(self, data: UBXMessage):
"""
@@ -366,6 +371,7 @@ def _process_NAV_SAT(self, data: UBXMessage):
:param UBXMessage data: NAV-SAT parsed message
"""
+ show_unused = self.__app.configuration.get("unusedsat_b")
self.__app.gnss_status.gsv_data = {}
num_siv = int(data.numSvs)
now = time()
@@ -384,6 +390,8 @@ def _process_NAV_SAT(self, data: UBXMessage):
elev = ""
azim = ""
cno = getattr(data, "cno" + idx)
+ if cno == 0 and not show_unused: # ignore sats with zero cno
+ continue
self.__app.gnss_status.gsv_data[(gnssId, svid)] = (
gnssId,
svid,
@@ -407,6 +415,7 @@ def _process_NAV_SIG(self, data: UBXMessage):
:param UBXMessage data: NAV-SIG parsed message
"""
+ show_unused = self.__app.configuration.get("unusedsat_b")
self.__app.gnss_status.sig_data = {}
num_sig = int(data.numSigs)
now = time()
@@ -420,6 +429,8 @@ def _process_NAV_SIG(self, data: UBXMessage):
if gnssId == 6 and svid < 25 and svid != 255 and GLONASS_NMEA:
svid += 64
cno = getattr(data, "cno" + idx)
+ if cno == 0 and not show_unused: # ignore sats with zero cno
+ continue
corrsource = getattr(data, "corrSource" + idx)
quality = getattr(data, "qualityInd" + idx)
sigflags = 0
@@ -515,7 +526,7 @@ def _process_NAV_RELPOSNED(self, data: UBXMessage):
"""
Process NAV-RELPOSNED sentence - Relative position of rover.
- Two version of this message:
+ Two versions of this message:
- version 0 has no heading or length attributes, so these
must be derived from the NED values
- version 1 has heading and length attributes
@@ -526,6 +537,12 @@ def _process_NAV_RELPOSNED(self, data: UBXMessage):
:param UBXMessage data: NAV-RELPOSNED parsed message
"""
+ if (
+ self.__app.configuration.get("relposnedsettings_d")["source_s"]
+ != data.identity
+ ):
+ return
+
if data.version == 0x00:
n = data.relPosN
e = data.relPosE
@@ -544,6 +561,7 @@ def _process_NAV_RELPOSNED(self, data: UBXMessage):
data.accHeading,
)
+ self.__app.gnss_status.rel_pos_source = data.identity
self.__app.gnss_status.rel_pos_heading = relPosHeading
self.__app.gnss_status.rel_pos_length = relPosLength
self.__app.gnss_status.acc_heading = accHeading
@@ -560,6 +578,55 @@ def _process_NAV_RELPOSNED(self, data: UBXMessage):
data.relPosNormalized,
]
+ def _process_NAV_DAHEADING(self, data: UBXMessage):
+ """
+ Process NAV-DAHEADING sentence - Relative position of dual antenna.
+
+ Two versions of this message:
+ - version 1 (HDG 2.00B1 pre-production) outputs in cm
+ - version 2 (HDG 2.00 production) outputs in mm
+
+ :param UBXMessage data: NAV-DAHEADING parsed message
+ """
+
+ if (
+ self.__app.configuration.get("relposnedsettings_d")["source_s"]
+ != data.identity
+ ):
+ return
+
+ if data.version == 0x01:
+ relPosLength, relPosHeading, accLength, accHeading = (
+ data.relPosLength, # cm
+ data.relPosHeading,
+ data.accLength * 1e-2,
+ data.accHeading,
+ )
+ else:
+ relPosLength, relPosHeading, accLength, accHeading = (
+ data.relPosLength / 10, # mm/10 = cm
+ data.relPosHeading,
+ data.accLength / 10,
+ data.accHeading,
+ )
+
+ self.__app.gnss_status.rel_pos_source = data.identity
+ self.__app.gnss_status.rel_pos_heading = relPosHeading
+ self.__app.gnss_status.rel_pos_length = relPosLength
+ self.__app.gnss_status.acc_heading = accHeading
+ self.__app.gnss_status.acc_length = accLength
+ self.__app.gnss_status.rel_pos_flags = [
+ data.gnssFixOK,
+ data.diffSoln,
+ data.relPosValid,
+ data.carrSoln,
+ 0,
+ 0,
+ 0,
+ data.relPosHeadingValid,
+ 0,
+ ]
+
def _process_HNR_PVT(self, data: UBXMessage):
"""
Process HNR-PVT sentence - High Rate Navigation position velocity time solution.
diff --git a/src/pygpsclient/ubx_legacy_config_dialog.py b/src/pygpsclient/ubx_legacy_config_dialog.py
new file mode 100644
index 00000000..59da1629
--- /dev/null
+++ b/src/pygpsclient/ubx_legacy_config_dialog.py
@@ -0,0 +1,219 @@
+"""
+ubx_config_dialog.py
+
+UBX configuration container dialog
+
+This is the pop-up dialog containing the various
+UBX configuration command frames.
+
+Supply initial settings via `config` keyword argument.
+
+NB: Individual UBX configuration commands do not have uniquely
+identifiable synchronous or asynchronous responses (e.g. unique
+txn ID). The way we keep tabs on confirmation status is to
+maintain a list of all commands sent and the responses they're
+expecting. When we receive a response, we check against the list
+of awaited responses of the same type and flag the first one we
+find as 'confirmed'.
+
+Created on 19 Sep 2020
+
+:author: semuadmin (Steve Smith)
+:copyright: 2020 semuadmin
+:license: BSD 3-Clause
+"""
+
+from tkinter import NSEW
+
+from pyubx2 import SET, UBXMessage
+
+from pygpsclient.dynamic_config_frame import Dynamic_Config_Frame
+from pygpsclient.globals import (
+ CONNECTED,
+ CONNECTED_SIMULATOR,
+ CONNECTED_SOCKET,
+ ERRCOL,
+ UBX_CFGMSG,
+ UBX_CFGOTHER,
+ UBX_CFGPRT,
+ UBX_CFGRATE,
+ UBX_MONHW,
+ UBX_MONRF,
+ UBX_MONVER,
+)
+from pygpsclient.hardware_info_frame import Hardware_Info_Frame
+from pygpsclient.strings import DLGTUBXLEGACY
+from pygpsclient.toplevel_dialog import ToplevelDialog
+from pygpsclient.ubx_msgrate_frame import UBX_MSGRATE_Frame
+from pygpsclient.ubx_port_frame import UBX_PORT_Frame
+from pygpsclient.ubx_solrate_frame import UBX_RATE_Frame
+
+
+class UBXLegacyConfigDialog(ToplevelDialog):
+ """,
+ UBXLegacyConfigDialog class.
+ """
+
+ def __init__(self, app, *args, **kwargs): # pylint: disable=unused-argument
+ """
+ Constructor.
+
+ :param Tk app: reference to main tkinter application
+ :param args: optional args to pass to parent class (not currently used)
+ :param kwargs: optional kwargs to pass to parent class (not currently used)
+ """
+
+ self.__app = app # Reference to main application class
+
+ super().__init__(app, DLGTUBXLEGACY)
+
+ self._cfg_msg_command = None
+ self._pending_confs = {}
+ self._recordmode = False
+
+ self._body()
+ self._do_layout()
+ self._reset()
+ self._attach_events()
+ self._finalise()
+
+ def _body(self):
+ """
+ Set up frame and widgets.
+ """
+
+ # add configuration widgets
+ self.frm_device_info = Hardware_Info_Frame(
+ self.__app, self, protocol="UBX", borderwidth=2, relief="groove"
+ )
+ self._frm_config_port = UBX_PORT_Frame(
+ self.__app, self, borderwidth=2, relief="groove"
+ )
+ self._frm_config_rate = UBX_RATE_Frame(
+ self.__app, self, borderwidth=2, relief="groove"
+ )
+ self._frm_config_msg = UBX_MSGRATE_Frame(
+ self.__app, self, borderwidth=2, relief="groove"
+ )
+ self._frm_config_dynamic = Dynamic_Config_Frame(
+ self.__app, self, protocol="UBX", borderwidth=2, relief="groove"
+ )
+
+ def _do_layout(self):
+ """
+ Position widgets in frame.
+ """
+
+ self.frm_device_info.grid(column=0, row=0, columnspan=3, sticky=NSEW)
+ self._frm_config_port.grid(column=0, row=1, sticky=NSEW)
+ self._frm_config_rate.grid(column=0, row=2, sticky=NSEW)
+ self._frm_config_msg.grid(column=0, row=3, sticky=NSEW)
+ self._frm_config_dynamic.grid(column=1, row=1, rowspan=3, sticky=NSEW)
+
+ for row in range(0, 2):
+ self.container.grid_columnconfigure(row, weight=1)
+ for row in range(1, 4):
+ self.container.grid_rowconfigure(row, weight=1)
+ self._frm_config_msg.grid_rowconfigure(3, weight=1)
+ self._frm_config_dynamic.grid_columnconfigure(0, weight=1)
+ self._frm_config_dynamic.grid_rowconfigure(1, weight=1)
+
+ def _reset(self):
+ """
+ Reset configuration widgets.
+ """
+
+ self._frm_config_rate.reset()
+ self._frm_config_port.reset()
+ self._frm_config_dynamic.reset()
+ self.frm_device_info.reset()
+ if self.__app.conn_status not in (
+ CONNECTED,
+ CONNECTED_SOCKET,
+ CONNECTED_SIMULATOR,
+ ):
+ self.set_status_label("Device not connected", ERRCOL)
+
+ def _attach_events(self):
+ """
+ Bind events to window.
+ """
+
+ # self.bind("", self._on_resize)
+
+ def set_pending(self, msgid: int, ubxfrm: int):
+ """
+ Set pending confirmation flag for UBX configuration frame to
+ signify that it's waiting for a confirmation message.
+
+ :param int msgid: UBX message identity
+ :param int ubxfrm: integer representing UBX configuration frame (0-6)
+ """
+
+ self._pending_confs[msgid] = ubxfrm
+
+ def update_pending(self, msg: UBXMessage):
+ """
+ Receives polled confirmation message from the ubx_handler and
+ updates whichever UBX config frame is waiting for this confirmation.
+
+ :param UBXMessage msg: UBX config message
+ """
+
+ ubxfrm = self._pending_confs.get(msg.identity, None)
+
+ if ubxfrm is not None:
+ if ubxfrm in (UBX_MONVER, UBX_MONHW, UBX_MONRF):
+ self.frm_device_info.reset()
+ elif ubxfrm == UBX_CFGPRT:
+ self._frm_config_port.update_status(msg)
+ elif ubxfrm == UBX_CFGRATE:
+ self._frm_config_rate.update_status(msg)
+ elif ubxfrm == UBX_CFGMSG:
+ self._frm_config_msg.update_status(msg)
+ elif ubxfrm == UBX_CFGOTHER:
+ self._frm_config_dynamic.update_status(msg)
+
+ # reset all confirmation flags for this frame
+ for msgid in (msg.identity, "ACK-ACK", "ACK-NAK"):
+ if self._pending_confs.get(msgid, None) == ubxfrm:
+ self._pending_confs.pop(msgid)
+
+ @property
+ def recordmode(self) -> bool:
+ """
+ Getter for recording status.
+
+ :return: recording yes/no
+ :rtype: bool
+ """
+
+ return self._recordmode
+
+ @recordmode.setter
+ def recordmode(self, recordmode: bool):
+ """
+ Setter for record mode.
+
+ :param bool recordmode: recording yes/no
+ """
+
+ self._recordmode = recordmode
+
+ def send_command(self, msg: UBXMessage):
+ """
+ Send command to receiver.
+ """
+
+ self.__app.send_to_device(msg.serialize())
+ self._record_command(msg)
+
+ def _record_command(self, msg: UBXMessage):
+ """
+ Record command to memory if in 'record' mode.
+
+ :param bytes msg: configuration message
+ """
+
+ if self.__app.recording and msg.msgmode == SET:
+ self.__app.recorded_commands = msg
diff --git a/src/pygpsclient/ubx_msgrate_frame.py b/src/pygpsclient/ubx_msgrate_frame.py
index d19f918c..dba17987 100644
--- a/src/pygpsclient/ubx_msgrate_frame.py
+++ b/src/pygpsclient/ubx_msgrate_frame.py
@@ -15,9 +15,9 @@
from tkinter import (
EW,
LEFT,
+ NSEW,
VERTICAL,
Button,
- E,
Frame,
IntVar,
Label,
@@ -26,6 +26,7 @@
S,
Scrollbar,
Spinbox,
+ Tk,
W,
)
@@ -49,18 +50,17 @@ class UBX_MSGRATE_Frame(Frame):
UBX Message Rate configuration command panel.
"""
- def __init__(self, app: Frame, parent: Frame, *args, **kwargs):
+ def __init__(self, app: Tk, parent: Frame, *args, **kwargs):
"""
Constructor.
- :param Frame app: reference to main tkinter application
+ :param Tk app: reference to main tkinter application
:param Frame parent: reference to parent frame (config-dialog)
:param args: optional args to pass to Frame parent class
:param kwargs: optional kwargs to pass to Frame parent class
"""
self.__app = app # Reference to main application class
- self.__master = self.__app.appmaster # Reference to root class (Tk)
self.__container = parent
super().__init__(parent.container, *args, **kwargs)
@@ -87,13 +87,13 @@ def _body(self):
self,
border=2,
relief="sunken",
- height=10,
+ height=8,
justify=LEFT,
exportselection=False,
)
self._scr_cfg_msg = Scrollbar(self, orient=VERTICAL)
- self._lbx_cfg_msg.config(yscrollcommand=self._scr_cfg_msg.set)
- self._scr_cfg_msg.config(command=self._lbx_cfg_msg.yview)
+ self._lbx_cfg_msg["yscrollcommand"] = self._scr_cfg_msg.set
+ self._scr_cfg_msg["command"] = self._lbx_cfg_msg.yview
self._lbl_ddc = Label(self, text="I2C")
self._spn_ddc = Spinbox(
self,
@@ -146,7 +146,6 @@ def _body(self):
width=50,
fg=OKCOL,
command=self._on_send_cfg_msg,
- font=self.__app.font_md,
cursor=CLICK_CURSOR,
)
@@ -154,28 +153,22 @@ def _do_layout(self):
"""
Layout widgets.
"""
- self._lbl_cfg_msg.grid(column=0, row=0, columnspan=6, sticky=EW)
- self._lbx_cfg_msg.grid(
- column=0, row=1, columnspan=2, rowspan=10, padx=3, sticky=EW
- )
- self._scr_cfg_msg.grid(column=1, row=1, rowspan=10, sticky=(N, S, E))
- self._lbl_usb.grid(column=2, row=1, rowspan=2, sticky=E)
- self._spn_usb.grid(column=3, row=1, rowspan=2, sticky=W)
- self._lbl_uart1.grid(column=2, row=3, rowspan=2, sticky=E)
- self._spn_uart1.grid(column=3, row=3, rowspan=2, sticky=W)
- self._lbl_uart2.grid(column=2, row=5, rowspan=2, sticky=E)
- self._spn_uart2.grid(column=3, row=5, rowspan=2, sticky=W)
- self._lbl_ddc.grid(column=2, row=7, rowspan=2, sticky=E)
- self._spn_ddc.grid(column=3, row=7, rowspan=2, sticky=W)
- self._lbl_spi.grid(column=2, row=9, rowspan=2, sticky=E)
- self._spn_spi.grid(column=3, row=9, rowspan=2, sticky=W)
- self._btn_send_command.grid(
- column=4, row=1, rowspan=10, ipadx=3, ipady=3, sticky=E
- )
- self._lbl_send_command.grid(
- column=5, row=1, rowspan=10, ipadx=3, ipady=3, sticky=E
- )
- self.option_add("*Font", self.__app.font_sm)
+ self._lbl_cfg_msg.grid(column=0, row=0, columnspan=5, sticky=EW)
+ self._lbl_usb.grid(column=0, row=1, pady=0)
+ self._spn_usb.grid(column=0, row=2, pady=0)
+ self._lbl_uart1.grid(column=1, row=1, pady=0)
+ self._spn_uart1.grid(column=1, row=2, pady=0)
+ self._lbl_uart2.grid(column=2, row=1, pady=0)
+ self._spn_uart2.grid(column=2, row=2, pady=0)
+ self._lbl_ddc.grid(column=3, row=1, pady=0)
+ self._spn_ddc.grid(column=3, row=2, pady=0)
+ self._lbl_spi.grid(column=4, row=1, pady=0)
+ self._spn_spi.grid(column=4, row=2, pady=0)
+ self._btn_send_command.grid(column=5, row=0, ipadx=3, ipady=3, rowspan=2)
+ self._lbl_send_command.grid(column=6, row=0, rowspan=2)
+ self._lbx_cfg_msg.grid(column=0, row=3, columnspan=5, pady=2, sticky=NSEW)
+ self._scr_cfg_msg.grid(column=5, row=3, sticky=(N, S, W))
+ self.grid_rowconfigure(3, weight=1)
def _attach_events(self):
"""
@@ -209,11 +202,11 @@ def update_status(self, msg: UBXMessage):
self._uart2_rate.set(msg.rateUART2)
self._usb_rate.set(msg.rateUSB)
self._spi_rate.set(msg.rateSPI)
- self._lbl_send_command.config(image=self.__container.img_confirmed)
+ self._lbl_send_command["image"] = self.__container.img_confirmed
elif msg.identity == "ACK-NAK":
self.__container.status_label = ("CFG-MSG POLL message rejected", ERRCOL)
- self._lbl_send_command.config(image=self.__container.img_warn)
+ self._lbl_send_command["image"] = self.__container.img_warn
def _on_select_cfg_msg(self, *args, **kwargs): # pylint: disable=unused-argument
"""
@@ -253,7 +246,7 @@ def _on_send_cfg_msg(self, *args, **kwargs): # pylint: disable=unused-argument
rateSPI=rateSPI,
)
self.__container.send_command(msg)
- self._lbl_send_command.config(image=self.__container.img_pending)
+ self._lbl_send_command["image"] = self.__container.img_pending
self.__container.status_label = "CFG-MSG SET message sent"
for msgid in ("ACK-ACK", "ACK-NAK"):
self.__container.set_pending(msgid, UBX_CFGMSG)
@@ -269,7 +262,7 @@ 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.__container.img_pending)
+ self._lbl_send_command["image"] = self.__container.img_pending
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 1692f4f9..dc8b2c34 100644
--- a/src/pygpsclient/ubx_port_frame.py
+++ b/src/pygpsclient/ubx_port_frame.py
@@ -20,6 +20,7 @@
Label,
Spinbox,
StringVar,
+ Tk,
W,
)
@@ -43,18 +44,17 @@ class UBX_PORT_Frame(Frame):
UBX Port and Protocol configuration command panel.
"""
- def __init__(self, app: Frame, parent: Frame, *args, **kwargs):
+ def __init__(self, app: Tk, parent: Frame, *args, **kwargs):
"""
Constructor.
- :param Frame app: reference to main tkinter application
+ :param Tk app: reference to main tkinter application
:param Frame parent: reference to parent frame (config-dialog)
:param args: optional args to pass to Frame parent class
:param kwargs: optional kwargs to pass to Frame parent class
"""
self.__app = app # Reference to main application class
- self.__master = self.__app.appmaster # Reference to root class (Tk)
self.__container = parent
super().__init__(parent.container, *args, **kwargs)
@@ -127,7 +127,6 @@ def _body(self):
image=self.__container.img_send,
width=50,
command=self._on_send_port,
- font=self.__app.font_md,
cursor=CLICK_CURSOR,
)
@@ -156,7 +155,6 @@ def _do_layout(self):
self._lbl_send_command.grid(
column=4, row=1, rowspan=2, ipadx=3, ipady=3, sticky=NE
)
- self.option_add("*Font", self.__app.font_sm)
def _attach_events(self):
"""
@@ -190,12 +188,12 @@ def update_status(self, msg: UBXMessage):
self._outprot_ubx.set(msg.outUBX)
self._outprot_nmea.set(msg.outNMEA)
self._outprot_rtcm3.set(msg.outRTCM3)
- self._lbl_send_command.config(image=self.__container.img_confirmed)
+ self._lbl_send_command["image"] = self.__container.img_confirmed
self.__container.status_label = ("CFG-PRT GET message received", OKCOL)
elif msg.identity == "ACK-NAK":
self.__container.status_label = ("CFG-PRT POLL message rejected", ERRCOL)
- self._lbl_send_command.config(image=self.__container.img_warn)
+ self._lbl_send_command["image"] = self.__container.img_warn
def _on_select_portid(self):
"""
@@ -236,7 +234,7 @@ def _on_send_port(self, *args, **kwargs): # pylint: disable=unused-argument
outRTCM3=outRTCM3,
)
self.__container.send_command(msg)
- self._lbl_send_command.config(image=self.__container.img_pending)
+ self._lbl_send_command["image"] = self.__container.img_pending
self.__container.status_label = "CFG-PRT SET message sent"
for msgid in ("ACK-NAK", "ACK-NAK"):
self.__container.set_pending(msgid, UBX_CFGPRT)
@@ -251,7 +249,7 @@ def _do_poll_prt(self, *args, **kwargs): # pylint: disable=unused-argument
portID = int(self._portid.get()[0:1])
msg = UBXMessage("CFG", "CFG-PRT", POLL, portID=portID)
self.__container.send_command(msg)
- self._lbl_send_command.config(image=self.__container.img_pending)
+ self._lbl_send_command["image"] = self.__container.img_pending
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 51d6ebd1..29ca35dc 100644
--- a/src/pygpsclient/ubx_preset_frame.py
+++ b/src/pygpsclient/ubx_preset_frame.py
@@ -28,6 +28,7 @@
S,
Scrollbar,
StringVar,
+ Tk,
W,
)
@@ -39,9 +40,7 @@
ERRCOL,
OKCOL,
UBX_PRESET,
- VALREGEX,
)
-from pygpsclient.helpers import validate # pylint: disable=unused-import
from pygpsclient.strings import (
CONFIRM,
DLGACTION,
@@ -60,18 +59,17 @@ class UBX_PRESET_Frame(Frame):
UBX Preset and User-defined configuration command panel.
"""
- def __init__(self, app: Frame, parent: Frame, *args, **kwargs):
+ def __init__(self, app: Tk, parent: Frame, *args, **kwargs):
"""
Constructor.
- :param Frame app: reference to main tkinter application
+ :param Tk app: reference to main tkinter application
:param Frame parent: reference to parent frame (config-dialog)
:param args: optional args to pass to Frame parent class
:param kwargs: optional kwargs to pass to Frame parent class
"""
self.__app = app # Reference to main application class
- self.__master = self.__app.appmaster # Reference to root class (Tk)
self.logger = logging.getLogger(__name__)
self.__container = parent
@@ -113,10 +111,10 @@ def _body(self):
)
self._scr_presetv = Scrollbar(self, orient=VERTICAL)
self._scr_preseth = Scrollbar(self, orient=HORIZONTAL)
- self._lbx_preset.config(yscrollcommand=self._scr_presetv.set)
- self._lbx_preset.config(xscrollcommand=self._scr_preseth.set)
- self._scr_presetv.config(command=self._lbx_preset.yview)
- self._scr_preseth.config(command=self._lbx_preset.xview)
+ self._lbx_preset["yscrollcommand"] = self._scr_presetv.set
+ self._lbx_preset["xscrollcommand"] = self._scr_preseth.set
+ self._scr_presetv["command"] = self._lbx_preset.yview
+ self._scr_preseth["command"] = self._lbx_preset.xview
self._lbl_send_command = Label(self, image=self.__container.img_none)
self._btn_send_command = Button(
self,
@@ -131,15 +129,14 @@ def _do_layout(self):
Layout widgets.
"""
- self._lbl_command.grid(column=0, row=0, sticky=W)
- self._ent_command.grid(column=1, row=0, columnspan=4, sticky=EW)
- self._lbl_presets.grid(column=0, row=1, columnspan=5, sticky=EW)
+ self._lbl_presets.grid(column=0, row=0, columnspan=5, sticky=EW)
+ self._lbl_command.grid(column=0, row=1, sticky=W)
+ self._ent_command.grid(column=1, row=1, columnspan=4, sticky=EW)
self._lbx_preset.grid(column=0, row=2, columnspan=2, sticky=NSEW)
self._scr_presetv.grid(column=2, row=2, sticky=(N, S, E))
self._scr_preseth.grid(column=0, row=3, columnspan=2, sticky=EW)
self._btn_send_command.grid(column=3, row=2, ipadx=3, ipady=3, sticky=NE)
self._lbl_send_command.grid(column=4, row=2, ipadx=3, ipady=3, sticky=NE)
- self.option_add("*Font", self.__app.font_sm)
def _attach_events(self):
"""
@@ -173,9 +170,6 @@ def _on_send_preset(self, *args, **kwargs): # pylint: disable=unused-argument
Preset command send button has been clicked.
"""
- if not self._ent_command.validate(VALREGEX, regex=UBXPRESETREGEX):
- self.__container.status_label = ("Invalid command format", ERRCOL)
- return
self._preset_command = self._command.get()
status = CONFIRMED
@@ -193,7 +187,7 @@ def _on_send_preset(self, *args, **kwargs): # pylint: disable=unused-argument
status = CONFIRMED
if status == CONFIRMED:
- self._lbl_send_command.config(image=self.__container.img_pending)
+ self._lbl_send_command["image"] = self.__container.img_pending
self.__container.status_label = "Command(s) sent"
for msgid in confids:
self.__container.set_pending(msgid, UBX_PRESET)
@@ -204,7 +198,7 @@ def _on_send_preset(self, *args, **kwargs): # pylint: disable=unused-argument
except Exception as err: # pylint: disable=broad-except
self.__container.status_label = (f"Error {err}", ERRCOL)
- self._lbl_send_command.config(image=self.__container.img_warn)
+ self._lbl_send_command["image"] = self.__container.img_warn
def _format_preset(self, command: str):
"""
@@ -232,7 +226,7 @@ def _format_preset(self, command: str):
self.__container.send_command(msg)
except Exception as err: # pylint: disable=broad-except
self.__container.status_label = (f"Error {err}", ERRCOL)
- self._lbl_send_command.config(image=self.__container.img_warn)
+ self._lbl_send_command["image"] = self.__container.img_warn
def update_status(self, msg: UBXMessage):
"""
@@ -242,8 +236,8 @@ def update_status(self, msg: UBXMessage):
"""
if msg.identity in ("ACK-ACK", "MON-VER"):
- self._lbl_send_command.config(image=self.__container.img_confirmed)
+ self._lbl_send_command["image"] = self.__container.img_confirmed
self.__container.status_label = ("Preset command(s) acknowledged", OKCOL)
elif msg.identity == "ACK-NAK":
- self._lbl_send_command.config(image=self.__container.img_warn)
+ self._lbl_send_command["image"] = self.__container.img_warn
self.__container.status_label = ("Preset command(s) rejected", ERRCOL)
diff --git a/src/pygpsclient/ubx_solrate_frame.py b/src/pygpsclient/ubx_solrate_frame.py
index 291c6081..6d8ea232 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 EW, Button, E, Frame, IntVar, Label, Spinbox, StringVar, W
+from tkinter import EW, Button, E, Frame, IntVar, Label, Spinbox, StringVar, Tk, W
from pyubx2 import POLL, SET, UBXMessage
@@ -38,18 +38,17 @@ class UBX_RATE_Frame(Frame):
UBX Navigation Solution Rate configuration command panel.
"""
- def __init__(self, app: Frame, parent: Frame, *args, **kwargs):
+ def __init__(self, app: Tk, parent: Frame, *args, **kwargs):
"""
Constructor.
- :param Frame app: reference to main tkinter application
+ :param Tk app: reference to main tkinter application
:param Frame parent: reference to parent frame (config-dialog)
:param args: optional args to pass to Frame parent class
:param kwargs: optional kwargs to pass to Frame parent class
"""
self.__app = app # Reference to main application class
- self.__master = self.__app.appmaster # Reference to root class (Tk)
self.__container = parent
super().__init__(parent.container, *args, **kwargs)
@@ -102,7 +101,6 @@ def _body(self):
image=self.__container.img_send,
width=50,
command=self._on_send_rate,
- font=self.__app.font_md,
cursor=CLICK_CURSOR,
)
@@ -124,7 +122,6 @@ def _do_layout(self):
self._lbl_send_command.grid(
column=5, row=1, rowspan=3, ipadx=3, ipady=3, sticky=E
)
- self.option_add("*Font", self.__app.font_sm)
def _attach_events(self):
"""
@@ -153,12 +150,12 @@ def update_status(self, msg: UBXMessage):
self._measint.set(msg.measRate)
self._navrate.set(msg.navRate)
self._timeref.set(TIMEREFS[msg.timeRef])
- self._lbl_send_command.config(image=self.__container.img_confirmed)
+ self._lbl_send_command["image"] = self.__container.img_confirmed
self.__container.status_label = ("CFG-RATE GET message received", OKCOL)
elif msg.identity == "ACK-NAK":
self.__container.status_label = ("CFG-RATE POLL message rejected", ERRCOL)
- self._lbl_send_command.config(image=self.__container.img_warn)
+ self._lbl_send_command["image"] = self.__container.img_warn
def _on_send_rate(self, *args, **kwargs): # pylint: disable=unused-argument
"""
@@ -180,7 +177,7 @@ def _on_send_rate(self, *args, **kwargs): # pylint: disable=unused-argument
timeRef=tref,
)
self.__container.send_command(msg)
- self._lbl_send_command.config(image=self.__container.img_pending)
+ self._lbl_send_command["image"] = self.__container.img_pending
self.__container.status_label = "CFG-RATE SET message sent"
self.__container.set_pending(UBX_CFGRATE, ("ACK-ACK", "ACK-NAK"))
@@ -193,7 +190,7 @@ 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.__container.img_pending)
+ self._lbl_send_command["image"] = self.__container.img_pending
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/uni_handler.py b/src/pygpsclient/uni_handler.py
index 5d9c408a..9afe5d35 100644
--- a/src/pygpsclient/uni_handler.py
+++ b/src/pygpsclient/uni_handler.py
@@ -54,11 +54,10 @@ def __init__(self, app):
"""
Constructor.
- :param Frame app: reference to main tkinter application
+ :param Tk app: reference to main tkinter application
"""
self.__app = app # Reference to main application class
- self.__master = self.__app.appmaster # Reference to root class (Tk)
self.logger = logging.getLogger(__name__)
self._raw_data = None
@@ -77,7 +76,9 @@ def process_data(self, raw_data: bytes, parsed_data: UNIMessage):
return
if self.__app.gnss_status.version_data["hwversion"] == NA:
self.__app.gnss_status.version_data["hwversion"] = "Unicore"
- self.__app.device_label = self.__app.gnss_status.version_data["hwversion"]
+ self.__app.set_device_label(
+ self.__app.gnss_status.version_data["hwversion"]
+ )
# self.logger.debug(f"data received {parsed_data.identity}")
self._process_utc(parsed_data)
if parsed_data.identity in ("BESTNAV", "BESTNAVH"):
@@ -188,6 +189,7 @@ def _process_SATSINFO(self, data: UNIMessage):
:param UNIMessage data: SATSINFO parsed message
"""
+ show_unused = self.__app.configuration.get("unusedsat_b")
self.__app.gnss_status.gsv_data = {}
num_siv = int(data.numsat)
now = time()
@@ -199,6 +201,8 @@ def _process_SATSINFO(self, data: UNIMessage):
elev = getattr(data, "elev" + idx)
azim = getattr(data, "azi" + idx)
cno = getattr(data, "cno" + idx + "_01")
+ if cno == 0 and not show_unused:
+ continue
self.__app.gnss_status.gsv_data[(gnssId, svid)] = (
gnssId,
svid,
@@ -267,4 +271,4 @@ def _process_VERSION(self, data: UNIMessage):
self.__app.gnss_status.version_data["fwversion"] = data.comptime
self.__app.gnss_status.version_data["romversion"] = NA
self.__app.gnss_status.version_data["gnss"] = NA
- self.__app.device_label = self.__app.gnss_status.version_data["hwversion"]
+ self.__app.set_device_label(self.__app.gnss_status.version_data["hwversion"])
diff --git a/src/pygpsclient/widget_state.py b/src/pygpsclient/widget_state.py
index 424ff930..62b8f966 100644
--- a/src/pygpsclient/widget_state.py
+++ b/src/pygpsclient/widget_state.py
@@ -49,7 +49,7 @@ class definition and update `ubx_handler` to populate them.
WDGCONSOLE = "Console"
WDGLEVELS = "Levels"
WDGMAP = "Map"
-WDGROVER = "Rover Plot"
+WDGROVER = "Relative Position"
WDGSATS = "Satellites"
WDGSCATTER = "Scatter Plot"
WDGSPECTRUM = "Spectrum"
diff --git a/tests/test_static.py b/tests/test_static.py
index 5da933bc..f40a8832 100644
--- a/tests/test_static.py
+++ b/tests/test_static.py
@@ -106,17 +106,11 @@ class DummyApp: # Dummy App class
def __init__(self):
- self.appmaster = "appmaster"
self.widget_state = WidgetState()
self.file_handler = DummyFileHandler()
self.label_status = ""
- @property
- def status_label(self) -> str:
- return self.label_status
-
- @status_label.setter
- def status_label(self, message):
+ def set_status_label(self, message, col="blue"):
print(message)
@@ -143,7 +137,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), 157)
+ self.assertEqual(len(cfg.settings), 159)
kwargs = {"userport": "/dev/ttyACM0", "spartnport": "/dev/ttyACM1"}
cfg.loadcli(**kwargs)
self.assertEqual(cfg.get("userport_s"), "/dev/ttyACM0")