diff --git a/INSTALLATION.md b/INSTALLATION.md
index 76581c46..4c200f0e 100644
--- a/INSTALLATION.md
+++ b/INSTALLATION.md
@@ -1,4 +1,3 @@
-
# PyGPSClient Installation
[Basics](#basics) |
@@ -92,25 +91,43 @@ brew install python-tk@3.13 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)
-Some Linux distributions may not include the necessary pip, tkinter, Pillow or spatialite libraries by default. They may need to be installed separately, e.g.:
+Some Linux distributions may not include the necessary pip, venv, tkinter, Pillow or spatialite libraries by default. They may need to be installed separately, e.g. for Debian-based distibutions, a combination of some or all of the following:
+
+```shell
+sudo apt install python3-pip python3-tk python3-pil python3-venv python3-pil.imagetk libjpeg-dev zlib1g-dev tk-dev libspatialite
+```
+
+For Arch-based distributions:
```shell
-sudo apt install python3-pip python3-tk python3-pil python3-pil.imagetk libjpeg-dev zlib1g-dev tk-dev libspatialite
+sudo pacman -S tk libspatialite
```
⁴ Support for the sqlite3 `mod_spatialite` extension may require a custom version of Python to be [compiled from source](https://github.com/semuconsulting/PyGPSClient/blob/master/examples/python_compile.sh) if a suitable version is not available from any of the distribution's repos.
## User Privileges
-To access the serial port on most Linux platforms, you will need to be a member of the
-`tty` and/or `dialout` groups. Other than this, no special privileges are required.
+To access the serial port (`/dev/tty*`) on most Linux platforms, you will need to be a member of whichever group or "`Gid`" the `/dev/tty*` device belongs to. Failure to do this will typically result in an error `[Errno 13] could not open port /dev/ttyACM0 [Errno 13] permission denied /dev/ttyACM0`
+
+To check and set the necessary group permissions:
+
+```shell
+stat /dev/ttyACM0 | grep Gid
+```
+`Access: (0660/crw-rw----) Uid: ( 0/ root) Gid: ( 20/ dialout)` <-- group in this case is `dialout`; add user to this group using usermod (*you may have to log out and in again for this to take effect*):
```shell
-usermod -a -G tty myuser
+sudo usermod -a -G dialout myuser
```
+For Debian-based platforms, the group is normally `dialout`; on Arch-based platforms it may be `uucp` or `tty`.
+
+Other than this, no special privileges are required.
+
## Install using pip
The recommended way to install the latest version of `PyGPSClient` is with [pip](http://pypi.python.org/pypi/pip/):
diff --git a/README.md b/README.md
index 6f6c3a8d..ac7442a9 100644
--- a/README.md
+++ b/README.md
@@ -23,8 +23,8 @@ PyGPSClient is a free, open-source, multi-platform graphical GNSS/GPS testing, d
* Supports NMEA, UBX, SBF, QGC, RTCM3, NTRIP, SPARTN, MQTT and TTY (ASCII) protocols¹.
* Capable of reading from a variety of GNSS data streams: Serial (USB / UART), Socket (TCP / UDP), binary data stream (terminal or file capture) and binary recording (e.g. u-center \*.ubx).
* Provides [NTRIP](#ntripconfig) client facilities.
-* Can serve as an [NTRIP base station](#basestation) with an RTK-compatible receiver (e.g. u-blox ZED-F9P/ZED-X20P, Quectel LG290P/LG580P/LC29H and Septentrio Mosaic G5/X5).
-* Supports GNSS (*and related*) device configuration via proprietary UBX, NMEA and ASCII TTY protocols, including most u-blox, Quectel, Septentrio and Feyman GNSS devices.
+* Can serve as an [NTRIP base station](#basestation) with an RTK-compatible receiver (e.g. u-blox ZED-F9P/ZED-X20P, Quectel LG290P/LG580P/LC29H, Septentrio Mosaic G5/X5 or Unicore UM98n).
+* Supports GNSS (*and related*) device configuration via proprietary UBX, NMEA and ASCII TTY protocols, including most u-blox, Quectel, Septentrio, Unicore and Feyman GNSS devices.
* Can be installed using the standard `pip` Python package manager - see [installation instructions](#installation) below.
This is an independent project and we have no affiliation whatsoever with any GNSS manufacturer or distributor.
@@ -104,6 +104,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.
1. 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. The `Msg Mode` parameter defaults to `GET` i.e., periodic or poll response messages *from* a receiver. If you wish to parse streams of command or poll messages being sent *to* a receiver, set the `Msg Mode` to `SET` or `POLL`. An optional serial or socket stream inactivity timeout can also be set (in seconds; 0 = no timeout).
+
+ If you get a permissions error on attempting to connect to a serial port e.g. `[Errno 13] permission denied /dev/ttyACM0`, refer to the [Installation Guidelines - User Privileges](https://github.com/semuconsulting/PyGPSClient/blob/master/INSTALLATION.md#user-privileges).
1. A custom user-defined serial port can also be passed via the json configuration file setting `"userport_s":`, via environment variable `PYGPSCLIENT_USERPORT` or as a command line argument `--userport`. A special userport value of "ubxsimulator" invokes the experimental [`pyubxutils.UBXSimulator`](https://github.com/semuconsulting/pyubxutils/blob/main/src/pyubxutils/ubxsimulator.py) utility to emulate a GNSS NMEA/UBX serial stream.
1. To connect to a TCP or UDP socket, enter the server URL and port, select the protocol (defaults to TCP) and click
. For encrypted TLS connections, tick the 'TLS' checkbox. Tick the 'Self Sign' checkbox to accommodate self-signed TLS certification (*typically for test or demonstration services*).
@@ -353,7 +355,7 @@ By default, the server/caster binds to the host address '0.0.0.0' (IPv4) or '::'
1. Select NTRIP CASTER mode and (if necessary) enter the host IP address and port.
1. Select 'TLS' to enable an encrypted TLS (HTTPS) connection.
1. An additional expandable panel is made available to allow the user to configure a connected RTK-compatible receiver to operate in either `FIXED` or `SURVEY-IN` Base Station mode (*NB: parameters can only be amended while the caster is stopped*).
-1. Select the receiver type (currently u-blox ZED-F9*, u-blox ZED-X20*, Quectel LG290P, Quectel LC29HBA and Septentrio Mosaic X5 receivers are supported) and click the Send button to send the appropriate configuration commands to the receiver.
+1. Select the receiver type and click the Send button to send the appropriate configuration commands to the receiver.
1. **NB** Septentrio Mosaic X5: These receivers are configured via ASCII TTY commands - to monitor the command responses, set the console protocol to "TTY" (*remember to set it back to RTCM when monitoring the RTCM3 output*). Note also that the input (ASCII command) UART port may be different to the output (RTCM3) UART port - make sure to select the appropriate port(s) when configuring the device and monitoring the RTCM3 output.
1. NMEA messages can be suppressed by checking 'Disable NMEA'.
1. NTRIP client login credentials are set via the user and password fields.
diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md
index 672a0302..c5718b1c 100644
--- a/RELEASE_NOTES.md
+++ b/RELEASE_NOTES.md
@@ -1,5 +1,11 @@
# PyGPSClient Release Notes
+### RELEASE 1.6.2
+
+1. Add support for Unicore Secondary Antenna and Attitude (IMU) NMEA sentences e.g. UM98n "GGAH", "HPD" (requires pynmeagps>=1.1.0).
+1. Minor cosmetic UI enhancements to improve rendering on some Linux window managers (e.g. xfce).
+1. `Waiting for XXX data` alerts added to user-selectable widgets to clarify which type of GNSS data each widget is waiting for. Widgets are not 'initialised' (underlying grids & labels drawn) until this data arrives. As in previous versions, widgets which depend on protocol-specific data (e.g. UBX) will display a `Receiver does not appear to support XXX data` alert if requisite data isn't received after 10 navigation solutions.
+
### RELEASE 1.6.1
1. Updates to main application window geometry (size and position) handling. Current window geometry is now saved to json configuration file as `screengeom_s` (e.g. `"1373x798+71+44"`), and will be restored on restart. Default startup geometry is centered at 75% of screen resolution.
diff --git a/docs/pygpsclient.rst b/docs/pygpsclient.rst
index a4499183..833dc541 100644
--- a/docs/pygpsclient.rst
+++ b/docs/pygpsclient.rst
@@ -532,6 +532,14 @@ pygpsclient.ubx\_solrate\_frame module
:undoc-members:
:show-inheritance:
+pygpsclient.uni\_handler module
+-------------------------------
+
+.. automodule:: pygpsclient.uni_handler
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
pygpsclient.widget\_state module
--------------------------------
diff --git a/pyproject.toml b/pyproject.toml
index b28e73e7..297f985b 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -134,7 +134,7 @@ disable = """
[tool.pytest.ini_options]
minversion = "7.0"
-addopts = "--cov --cov-report html --cov-fail-under 16"
+addopts = "--cov --cov-report html --cov-fail-under 17"
pythonpath = ["src"]
testpaths = ["tests"]
@@ -145,7 +145,7 @@ source = ["src"]
source = ["src"]
[tool.coverage.report]
-fail_under = 16
+fail_under = 17
[tool.coverage.html]
directory = "htmlcov"
diff --git a/src/pygpsclient/__main__.py b/src/pygpsclient/__main__.py
index 1f6eec7d..076bc76d 100644
--- a/src/pygpsclient/__main__.py
+++ b/src/pygpsclient/__main__.py
@@ -8,7 +8,6 @@
:license: BSD 3-Clause
"""
-import sys
from argparse import SUPPRESS, ArgumentDefaultsHelpFormatter, ArgumentParser
from logging import getLogger
from tkinter import Tk
@@ -151,7 +150,6 @@ def main():
root = Tk()
App(root, **kwargs)
root.mainloop()
- sys.exit()
if __name__ == "__main__":
diff --git a/src/pygpsclient/_version.py b/src/pygpsclient/_version.py
index ffa05fa2..99d1f787 100644
--- a/src/pygpsclient/_version.py
+++ b/src/pygpsclient/_version.py
@@ -8,4 +8,4 @@
:license: BSD 3-Clause
"""
-__version__ = "1.6.1"
+__version__ = "1.6.2"
diff --git a/src/pygpsclient/about_dialog.py b/src/pygpsclient/about_dialog.py
index 44d8ec8a..9b4e6bb9 100644
--- a/src/pygpsclient/about_dialog.py
+++ b/src/pygpsclient/about_dialog.py
@@ -111,7 +111,7 @@ def _body(self):
self._btn_checkupdate = Button(
self._frm_body,
text="",
- width=14,
+ width=16,
cursor="hand2",
)
self._chk_checkupdate = Checkbutton(
diff --git a/src/pygpsclient/app.py b/src/pygpsclient/app.py
index db5427c6..25fa8e3c 100644
--- a/src/pygpsclient/app.py
+++ b/src/pygpsclient/app.py
@@ -9,7 +9,8 @@
- Loads configuration from json file (if available)
- Instantiates all frames, widgets, and protocol handlers.
- Maintains state of all user-selectable widgets.
-- Maintains state of all threaded dialog and protocol handler processes.
+- Maintains state of all Toplevel dialogs.
+- Maintains state of all threaded protocol handler processes.
- Maintains state of serial and RTK connections.
- Handles event-driven data processing of navigation data placed on
input message queue by stream handler and assigns to appropriate
@@ -33,7 +34,7 @@
:license: BSD 3-Clause
"""
-# pylint: disable=too-many-ancestors, no-member
+# pylint: disable=no-member
import logging
from datetime import datetime, timedelta
@@ -43,12 +44,11 @@
from subprocess import CalledProcessError, run
from sys import executable
from threading import Thread
-from time import process_time_ns, time
from tkinter import EW, NSEW, NW, Frame, Label, PhotoImage, Tk, Toplevel, font
from types import NoneType
from pygnssutils import GNSSMQTTClient, GNSSNTRIPClient, MQTTMessage
-from pygnssutils.gnssreader import (
+from pygnssutils.gnssreader import ( # UNI_PROTOCOL, # TODO
NMEA_PROTOCOL,
POLL,
QGC_PROTOCOL,
@@ -74,10 +74,7 @@
BGCOL,
CLASS,
CONFIGFILE,
- CONNECTED,
CONNECTED_NTRIP,
- CONNECTED_SIMULATOR,
- CONNECTED_SOCKET,
CONNECTED_SPARTNIP,
CONNECTED_SPARTNLB,
DISCONNECTED,
@@ -100,6 +97,7 @@
STATUSPRIORITY,
TTY_PROTOCOL,
UNDO,
+ UNI_PROTOCOL,
)
from pygpsclient.gnss_status import GNSSStatus
from pygpsclient.helpers import (
@@ -130,7 +128,6 @@
INTROTXTNOPORTS,
KILLSWITCH,
NOTCONN,
- NOWDGSWARN,
SAVECONFIGBAD,
SAVECONFIGOK,
TITLE,
@@ -141,12 +138,15 @@
)
from pygpsclient.tty_handler import TTYHandler
from pygpsclient.ubx_handler import UBXHandler
+from pygpsclient.uni_handler import UNIHandler
from pygpsclient.widget_state import (
COLSPAN,
DEFAULT,
+ DOCK,
HIDE,
MAXSPAN,
SHOW,
+ UNDOCK,
VISIBLE,
WDGCHART,
WDGCONSOLE,
@@ -159,7 +159,7 @@ class App(Frame):
Main PyGPSClient GUI Application Class.
"""
- def __init__(self, master, **kwargs): # pylint: disable=too-many-statements
+ def __init__(self, master, **kwargs):
"""
Set up main application and add frames.
@@ -168,9 +168,6 @@ def __init__(self, master, **kwargs): # pylint: disable=too-many-statements
:param kwargs: optional (CLI) kwargs
"""
- self.starttime = time() # for run time benchmarking
- self.processtime = 0 # for process time benchmarking
-
self.__master = master
self.logger = logging.getLogger(__name__)
@@ -187,9 +184,6 @@ def __init__(self, master, **kwargs): # pylint: disable=too-many-statements
self.configuration.loadcli(**kwargs)
if configerr == "":
self.update_widgets() # set initial widget state
- # warning if all widgets have been disabled in config
- if self._nowidgets:
- self.status_label = (NOWDGSWARN.format(configfile), ERRCOL)
# setup main application window
geom = self.configuration.get("screengeom_s")
@@ -216,6 +210,7 @@ def __init__(self, master, **kwargs): # pylint: disable=too-many-statements
self.ubx_handler = UBXHandler(self)
self.sbf_handler = SBFHandler(self)
self.qgc_handler = QGCHandler(self)
+ self.uni_handler = UNIHandler(self)
self.rtcm_handler = RTCM3Handler(self)
self.tty_handler = TTYHandler(self)
self.ntrip_handler = GNSSNTRIPClient(self)
@@ -224,11 +219,11 @@ def __init__(self, master, **kwargs): # pylint: disable=too-many-statements
self.frm_settings = None
self._conn_status = DISCONNECTED
self._rtk_conn_status = DISCONNECTED
- self._nowidgets = True
self._last_gui_update = datetime.now()
self._socket_thread = None
self._socket_server = None
self.consoledata = []
+ self.last_map_update = 0
self._recorded_commands = [] # captured by RecorderDialog
self.recording = False # RecordDialog status
self.recording_type = 0 # 0 = TTY ONLY, 1 = UBX/NMEA
@@ -246,13 +241,6 @@ def __init__(self, master, **kwargs): # pylint: disable=too-many-statements
self._do_layout()
self._attach_events()
- # initialise widgets
- for wdg in self.widget_state.state.values():
- frm = getattr(self, wdg[FRAME])
- if hasattr(frm, "init_frame"):
- frm.update_idletasks()
- frm.init_frame()
-
# display initial connection status
self.frm_banner.update_conn_status(DISCONNECTED)
if self.frm_settings.frm_serial.status == NOPORTS:
@@ -276,20 +264,15 @@ def _body(self):
self.menu = MenuBar(self)
self.__master.config(menu=self.menu, bg=BGCOL)
- self.frm_banner = BannerFrame(self, borderwidth=2, relief="groove")
- self.frm_status = StatusFrame(self, borderwidth=2, relief="groove")
- self.frm_settings = SettingsFrame(self)
+ 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)
- # instantiate widgets
- for wdg in self.widget_state.state.values():
- setattr(
- # self.frm_widgets,
- self,
- wdg[FRAME],
- wdg[CLASS](self, self.frm_widgets, borderwidth=2, relief="groove"),
- )
-
def _do_layout(self):
"""
Arrange and 'pack' visible widgets in main application frame and set
@@ -314,71 +297,49 @@ def _do_layout(self):
self.frm_banner.grid(column=0, row=0, columnspan=2, sticky=EW)
self.frm_widgets.grid(column=0, row=1, sticky=NSEW)
if isinstance(self.frm_settings, SettingsFrame): # docked
+ men0 = UNDOCK
if self.configuration.get("showsettings_b"):
self.frm_settings.grid(column=1, row=1, sticky=NW)
else:
if self.frm_settings.winfo_ismapped():
self.frm_settings.grid_forget()
+ else:
+ men0 = DOCK
self.frm_status.grid(column=0, row=2, columnspan=2, sticky=EW)
- # get overall column and row spans for frm_widgets
- maxcols = self.configuration.get("maxcolumns_n")
- wcolspan = 0
- wrowspan = 1
- cols = 0
- wids = [
- wdg.get(COLSPAN, 1)
- for wdg in self.widget_state.state.values()
- if wdg[VISIBLE]
- ]
- for c in wids:
- if c == MAXSPAN or c + cols > maxcols:
- wrowspan += 2 if c == MAXSPAN and cols > 0 else 1
- cols = 0
- cols += c
- wcolspan = max(wcolspan, cols)
- wcolspan = max(1, wcolspan)
+ # update View menu labels for Settings (dock/undock, show/hide)
+ self.menu.view_menu.entryconfig(0, label=f"{men0} Settings")
+ men1 = (SHOW, HIDE)[self.configuration.get("showsettings_b")]
+ self.menu.view_menu.entryconfig(1, label=f"{men1} Settings")
- # dynamically position widgets in frm_widgets
+ # get column and row spans for frm_widgets
+ wcolspan, wrowspan = self.widget_spans()
+
+ # dynamically instantiate and position widgets in frm_widgets
col = 0
row = 0
- men = 2
- for name, wdg in self.widget_state.state.items():
- frm = getattr(self, wdg[FRAME])
- if wdg[VISIBLE]:
- lbl = HIDE
- # enable any GNSS message output required by widget
- self.widget_enable_messages(name)
- c = wdg.get(COLSPAN, 1)
+ for i, (name, state) in enumerate(self.widget_state.state.items()):
+ # setup widget
+ frm = self.widget_setup(state)
+ # set corresponding View menu label to 'Show' or 'Hide'
+ self.menu.view_menu.entryconfig(
+ i + 2, label=f"{(SHOW, HIDE)[state[VISIBLE]]} {name}"
+ )
+ if state[VISIBLE]:
+ c = state.get(COLSPAN, 1)
cols = wcolspan if c == MAXSPAN else c
- # only grid if position has changed
frmi = frm.grid_info()
if (
frmi.get("column", None) != col
or frmi.get("row", None) != row
or frmi.get("columnspan", None) != cols
- ):
+ ): # only grid if position has changed
frm.grid(column=col, row=row, columnspan=cols, sticky=NSEW)
- if c == MAXSPAN or c + col >= maxcols:
+ if c == MAXSPAN or c + col >= wcolspan:
col = 0
row += 1
else:
col += c
- else:
- lbl = SHOW
- # only forget if gridded (memory leak!)
- if frm.winfo_ismapped():
- frm.grid_forget()
-
- # update View menu label (show/hide)
- self.menu.view_menu.entryconfig(men, label=f"{lbl} {name}")
- men += 1
-
- # update View menu labels for Settings (dock/undock, show/hide)
- lbl = "Undock" if self.configuration.get("docksettings_b") else "Dock"
- self.menu.view_menu.entryconfig(0, label=f"{lbl} Settings")
- lbl = HIDE if self.configuration.get("showsettings_b") else SHOW
- self.menu.view_menu.entryconfig(1, label=f"{lbl} Settings")
# set column and row weights to control 'pack' behaviour of main layout
self.__master.grid_columnconfigure(0, weight=1)
@@ -432,16 +393,71 @@ 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.frm_settings = SettingsFrame(self, self.__master)
else:
if self.dialog_state.state[DLGTSETTINGS][DLG] is None:
if isinstance(self.frm_settings, SettingsFrame):
self.frm_settings.grid_forget()
self.frm_settings.destroy()
+ self.frm_settings = None
self.start_dialog(DLGTSETTINGS)
self.frm_settings = self.dialog_state.state[DLGTSETTINGS][DLG]
self._do_layout()
+ def widget_setup(self, state: dict) -> Frame | NoneType:
+ """
+ Instantiate or destroy widget.
+
+ :param dict state: widget state dictionary
+ :return: instantiated widget or None
+ :rtype: Frame | NoneType
+ """
+
+ frm = state[FRAME]
+ visible = state[VISIBLE]
+ w = getattr(self, frm, None)
+ if visible and w is None:
+ setattr(
+ self,
+ frm,
+ state[CLASS](self, self.frm_widgets, borderwidth=1, relief="groove"),
+ )
+ elif not visible and w is not None:
+ w.grid_forget()
+ w.destroy()
+ w = None
+ setattr(self, frm, None)
+ return getattr(self, frm, None)
+
+ def widget_spans(self) -> tuple[int, int]:
+ """
+ Get overall column and row spans for frm_widgets
+ from latest widget_state dictionary.
+
+ :return: columnspan, rowspan
+ :rtype: tuple[int, int]
+ """
+
+ maxcols = self.configuration.get("maxcolumns_n")
+ wcolspan = 0
+ wrowspan = 0
+ cols = 0
+ wids = [
+ wdg.get(COLSPAN, 1)
+ for wdg in self.widget_state.state.values()
+ if wdg[VISIBLE]
+ ]
+ for c in wids:
+ if c == MAXSPAN or c + cols > maxcols:
+ wrowspan += 2 if c == MAXSPAN and cols > 0 else 1
+ cols = 0
+ cols += c
+ wcolspan = max(wcolspan, cols)
+ if cols > 0:
+ wrowspan += 1
+
+ return max(1, wcolspan), max(1, wrowspan)
+
def widget_toggle(self, name: str):
"""
Toggle widget visibility and enable or disable any
@@ -456,17 +472,6 @@ def widget_toggle(self, name: str):
self.configuration.set(name, self.widget_state.state[name][VISIBLE])
self._do_layout()
- def widget_enable_messages(self, name: str):
- """
- Enable any GNSS messages required by widget.
-
- :param str name: widget name
- """
-
- frm = getattr(self, self.widget_state.state[name][FRAME])
- if hasattr(frm, "enable_messages"):
- frm.enable_messages(self.widget_state.state[name][VISIBLE])
-
def reset_layout(self):
"""
Reset to default layout.
@@ -480,21 +485,6 @@ def reset_layout(self):
self.configuration.set("docksettings_b", True)
self._do_layout()
- def reset_frames(self):
- """
- Reset frames.
- """
-
- self.frm_mapview.reset_map_refresh()
- self.frm_spectrumview.reset()
-
- def reset_gnssstatus(self):
- """
- Reset gnss_status dictionary e.g. after reconnecting.
- """
-
- self.gnss_status = GNSSStatus()
-
def _set_default_fonts(self):
"""
Set default fonts for entire application.
@@ -528,7 +518,7 @@ def load_config(self):
self.status_label = (DLGSTOPRTK, ERRCOL)
return
- filename, err = self.configuration.loadfile()
+ _, err = self.configuration.loadfile()
if err == "": # load succeeded
self.update_widgets()
for frm in (
@@ -538,8 +528,6 @@ def load_config(self):
):
frm.reset()
self._do_layout()
- if self._nowidgets:
- self.status_label = (NOWDGSWARN.format(filename), ERRCOL)
elif err == "cancelled":
pass
@@ -568,16 +556,11 @@ def update_widgets(self):
"""
try:
- self._nowidgets = True
for key, vals in self.widget_state.state.items():
vis = self.configuration.get(key)
vals[VISIBLE] = vis
- if vis:
- self._nowidgets = False
- if self._nowidgets:
- self.widget_state.state["Status"][VISIBLE] = "true"
except KeyError as err:
- self.status_label = (f"{CONFIGERR} - {err}", ERRCOL)
+ self.status_label = (CONFIGERR.format(err), ERRCOL)
def _refresh_widgets(self):
"""
@@ -586,13 +569,14 @@ def _refresh_widgets(self):
self.frm_banner.update_frame()
for wdg, wdgdata in self.widget_state.state.items():
- frm = getattr(self, wdgdata[FRAME])
- if hasattr(frm, "update_frame") and wdgdata[VISIBLE]:
- if wdg == WDGCONSOLE:
- frm.update_frame(self.consoledata)
- self.consoledata = []
- else:
- frm.update_frame()
+ 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()
def start_dialog(self, dlg: str):
"""
@@ -607,7 +591,7 @@ def start_dialog(self, dlg: str):
def dialog(self, dlg: str) -> Toplevel:
"""
- Get reference to dialog instance.
+ Getter for dialog instance.
:param str dlg: name of dialog
:return: dialog instance
@@ -742,10 +726,10 @@ def on_killswitch(self, *args, **kwargs): # pylint: disable=unused-argument
try:
self._shutdown()
- for dlg in self.dialog_state.state:
- if self.dialog(dlg) is not None:
- self.dialog(dlg).destroy()
- # self.stop_dialog(dlg)
+ for name, state in self.dialog_state.state.items():
+ if self.dialog(name) is not None:
+ self.dialog(name).destroy()
+ state[DLG] = None
self.conn_status = DISCONNECTED
self.rtk_conn_status = DISCONNECTED
except Exception as err: # pylint: disable=broad-exception-caught
@@ -920,7 +904,6 @@ def process_data(self, raw_data: bytes, parsed_data: object, marker: str = ""):
:param str marker: string prepended to console entries e.g. "NTRIP>>"
"""
- start = process_time_ns()
# self.logger.debug(f"data received {parsed_data.identity}")
msgprot = 0
protfilter = self.protocol_mask
@@ -930,6 +913,8 @@ def process_data(self, raw_data: bytes, parsed_data: object, marker: str = ""):
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):
@@ -950,6 +935,8 @@ def process_data(self, raw_data: bytes, parsed_data: object, marker: str = ""):
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:
@@ -992,22 +979,17 @@ def process_data(self, raw_data: bytes, parsed_data: object, marker: str = ""):
self.file_handler.write_logfile(raw_data, parsed_data)
self.update_idletasks()
- self.processtime = process_time_ns() - start
- def send_to_device(self, data: object):
+ def send_to_device(self, data: bytes):
"""
- Send raw data to connected device.
+ Place raw data on output queue (it will be processed when the
+ device next connects).
- :param object data: raw GNSS data (NMEA, UBX, TTY, RTCM3, SPARTN)
+ :param bytes data: raw GNSS data (NMEA, UBX, TTY, RTCM3, SPARTN)
"""
self.logger.debug(f"Sending message {data}")
- if self.conn_status in (
- CONNECTED,
- CONNECTED_SOCKET,
- CONNECTED_SIMULATOR,
- ):
- self.gnss_outqueue.put(data)
+ self.gnss_outqueue.put(data)
def _check_update(self):
"""
@@ -1125,9 +1107,7 @@ def priority(col):
if hasattr(self, "frm_status"):
color = INFOCOL if color == "blue" else color
- self.status_label.after(
- 0, self.status_label.config, {"text": message, "fg": color}
- )
+ self.status_label.config(text=message, fg=color)
self.update_idletasks()
else: # defer message until frm_status is instantiated
if isinstance(self._deferredmsg, tuple):
@@ -1161,11 +1141,6 @@ def conn_status(self, status: int):
self.frm_settings.frm_settings.enable_controls(status)
if status == DISCONNECTED:
self.conn_label = (NOTCONN, INFOCOL)
- elif status in (CONNECTED, CONNECTED_SOCKET):
- for name, wdg in self.widget_state.state.items():
- if wdg[VISIBLE]:
- # enable any GNSS message output required by widget
- self.widget_enable_messages(name)
@property
def server_status(self) -> int:
@@ -1264,9 +1239,10 @@ def protocol_mask(self) -> int:
+ (cfg.get("rtcmprot_b") * RTCM3_PROTOCOL) # 4
+ (cfg.get("sbfprot_b") * SBF_PROTOCOL) # 8
+ (cfg.get("qgcprot_b") * QGC_PROTOCOL) # 16
- + (cfg.get("spartnprot_b") * SPARTN_PROTOCOL) # 32
- + (cfg.get("mqttprot_b") * MQTT_PROTOCOL) # 64
- + (cfg.get("ttyprot_b") * TTY_PROTOCOL) # 128
+ + (cfg.get("uniprot_b") * UNI_PROTOCOL) # 32
+ + (cfg.get("spartnprot_b") * SPARTN_PROTOCOL) # 256
+ + (cfg.get("mqttprot_b") * MQTT_PROTOCOL) # 512
+ + (cfg.get("ttyprot_b") * TTY_PROTOCOL) # 1024
)
return mask
@@ -1281,7 +1257,7 @@ def db_enabled(self) -> int | str:
return self._db_enabled
- def do_app_update(self, *args, **kwargs) -> int:
+ def do_app_update(self, *args, **kwargs) -> int: # pylint: disable=unused-argument
"""
Update outdated application packages to latest versions.
diff --git a/src/pygpsclient/banner_frame.py b/src/pygpsclient/banner_frame.py
index 48c266ba..f6cf6770 100644
--- a/src/pygpsclient/banner_frame.py
+++ b/src/pygpsclient/banner_frame.py
@@ -12,8 +12,7 @@
:license: BSD 3-Clause
"""
-from time import time
-from tkinter import NE, NW, SUNKEN, Button, E, Frame, Label, N, W
+from tkinter import NE, NW, SUNKEN, Button, Frame, Label, N, W
from PIL import Image, ImageTk
from pynmeagps.nmeahelpers import latlon2dmm, latlon2dms, llh2ecef
@@ -56,10 +55,11 @@
scale_font,
unused_sats,
)
-from pygpsclient.strings import NA
+from pygpsclient.strings import DGPSYES, NA
-DGPSYES = "YES"
M2MILES = 5280
+FONTBASE = 12
+FONTSCALE = 90
class BannerFrame(Frame):
@@ -67,7 +67,7 @@ class BannerFrame(Frame):
Banner frame class.
"""
- def __init__(self, app, *args, **kwargs):
+ def __init__(self, app: Frame, parent: Frame, *args, **kwargs):
"""
Constructor.
@@ -80,14 +80,8 @@ def __init__(self, app, *args, **kwargs):
self.__app = app # Reference to main application class
self.__master = self.__app.appmaster # Reference to root class (Tk)
- super().__init__(self.__master, *args, **kwargs)
-
self._status = False
self._show_advanced = False
-
- self._bgcol = BGCOL
- self._fgcol = FGCOL
- self.config(bg=self._bgcol)
self._img_conn = ImageTk.PhotoImage(Image.open(ICON_CONN))
self._img_serial = ImageTk.PhotoImage(Image.open(ICON_SERIAL))
self._img_socket = ImageTk.PhotoImage(Image.open(ICON_SOCKET))
@@ -101,6 +95,8 @@ def __init__(self, app, *args, **kwargs):
self._img_spartn = ImageTk.PhotoImage(Image.open(ICON_SPARTNCONFIG))
self._img_blank = ImageTk.PhotoImage(Image.open(ICON_BLANK))
+ super().__init__(parent, *args, **kwargs)
+
self.width, self.height = self.get_size()
self._body()
@@ -122,28 +118,24 @@ def _body(self):
self._frm_advanced = Frame(self, bg=BGCOL)
self._frm_advanced2 = Frame(self, bg=BGCOL)
- self.option_add("*Font", self.__app.font_md2)
self._lbl_clients = Label(
- self._frm_connect, bg=self._bgcol, fg="green", width=2, anchor=W
+ self._frm_connect, bg=BGCOL, fg="green", width=2, anchor=W
)
self._lbl_ltime = Label(
self._frm_basic,
text="utc:",
- bg=self._bgcol,
- fg=self._fgcol,
+ bg=BGCOL,
+ fg=FGCOL,
anchor=N,
)
self._lbl_llat = Label(
- self._frm_basic, text="lat:", bg=self._bgcol, fg=self._fgcol, anchor=N
+ self._frm_basic, text="lat:", bg=BGCOL, fg=FGCOL, anchor=N
)
self._lbl_llon = Label(
- self._frm_basic, text="lon:", bg=self._bgcol, fg=self._fgcol, anchor=N
- )
- self._lbl_lalt = Label(
- self._frm_basic, text="hmsl:", bg=self._bgcol, fg=self._fgcol, anchor=N
+ self._frm_basic, text="lon:", bg=BGCOL, fg=FGCOL, anchor=N
)
self._lbl_lfix = Label(
- self._frm_basic, text="fix:", bg=self._bgcol, fg=self._fgcol, anchor=N
+ self._frm_basic, text="fix:", bg=BGCOL, fg=FGCOL, anchor=N
)
self._btn_toggle = Button(
self._frm_toggle,
@@ -152,101 +144,93 @@ def _body(self):
command=self._toggle_advanced,
image=self._img_expand,
)
+ self._lbl_lhmsl = Label(
+ self._frm_advanced, text="hmsl:", bg=BGCOL, fg=FGCOL, anchor=N
+ )
self._lbl_lhae = Label(
- self._frm_advanced, text="hae:", bg=self._bgcol, fg=self._fgcol, anchor=N
+ self._frm_advanced, text="hae:", bg=BGCOL, fg=FGCOL, anchor=N
)
self._lbl_lspd = Label(
- self._frm_advanced, text="speed:", bg=self._bgcol, fg=self._fgcol, anchor=N
+ self._frm_advanced, text="speed:", bg=BGCOL, fg=FGCOL, anchor=N
)
self._lbl_ltrk = Label(
- self._frm_advanced, text="track:", bg=self._bgcol, fg=self._fgcol, anchor=N
+ self._frm_advanced, text="track:", bg=BGCOL, fg=FGCOL, anchor=N
)
self._lbl_lsiv = Label(
- self._frm_advanced2, text="siv:", bg=self._bgcol, fg=self._fgcol, anchor=N
+ self._frm_advanced2, text="siv:", bg=BGCOL, fg=FGCOL, anchor=N
)
self._lbl_lsip = Label(
- self._frm_advanced2, text="sip:", bg=self._bgcol, fg=self._fgcol, anchor=N
+ self._frm_advanced2, text="sip:", bg=BGCOL, fg=FGCOL, anchor=N
)
self._lbl_lpdop = Label(
- self._frm_advanced2, text="pdop:", bg=self._bgcol, fg=self._fgcol, anchor=N
+ self._frm_advanced2, text="pdop:", bg=BGCOL, fg=FGCOL, anchor=N
)
self._lbl_lacc = Label(
- self._frm_advanced2, text="acc:", bg=self._bgcol, fg=self._fgcol, anchor=N
+ self._frm_advanced2, text="acc:", bg=BGCOL, fg=FGCOL, anchor=N
)
self._lbl_lcorr = Label(
self._frm_advanced2,
text="corr:",
- bg=self._bgcol,
- fg=self._fgcol,
+ bg=BGCOL,
+ fg=FGCOL,
anchor=N,
)
- self.option_add("*Font", self.__app.font_lg)
self._lbl_status_preset = Label(
- self._frm_connect, bg=self._bgcol, image=self._img_conn
- )
- self._lbl_rtk_preset = Label(
- self._frm_connect, bg=self._bgcol, image=self._img_blank
+ self._frm_connect, bg=BGCOL, image=self._img_conn
)
+ self._lbl_rtk_preset = Label(self._frm_connect, bg=BGCOL, image=self._img_blank)
self._lbl_transmit_preset = Label(
- self._frm_connect, bg=self._bgcol, image=self._img_blank
- )
- self._lbl_time = Label(
- self._frm_basic, bg=self._bgcol, fg="cyan", width=15, anchor=W
+ self._frm_connect, bg=BGCOL, image=self._img_blank
)
+ self._lbl_time = Label(self._frm_basic, bg=BGCOL, fg="cyan", width=15, anchor=W)
self._lbl_lat = Label(
- self._frm_basic, bg=self._bgcol, fg="orange", width=16, anchor=W
+ self._frm_basic, bg=BGCOL, fg="orange", width=16, anchor=W
)
self._lbl_lon = Label(
- self._frm_basic, bg=self._bgcol, fg="orange", width=17, anchor=W
+ self._frm_basic, bg=BGCOL, fg="orange", width=17, anchor=W
)
- self._lbl_alt = Label(
- self._frm_basic, bg=self._bgcol, fg="orange", width=13, anchor=W
- )
- self._lbl_fix = Label(
- self._frm_basic, bg=self._bgcol, fg="white", width=10, anchor=W
+ self._lbl_fix = Label(self._frm_basic, bg=BGCOL, fg="white", width=10, anchor=W)
+ self._lbl_hmsl = Label(
+ self._frm_advanced, bg=BGCOL, fg="orange", width=13, anchor=W
)
self._lbl_hae = Label(
- self._frm_advanced, bg=self._bgcol, fg="orange", width=13, anchor=W
+ self._frm_advanced, bg=BGCOL, fg="orange", width=13, anchor=W
)
self._lbl_spd = Label(
- self._frm_advanced, bg=self._bgcol, fg="deepskyblue", width=12, anchor=W
+ self._frm_advanced, bg=BGCOL, fg="deepskyblue", width=12, anchor=W
)
self._lbl_trk = Label(
- self._frm_advanced, bg=self._bgcol, fg="deepskyblue", width=8, anchor=W
- )
- self._lbl_benchmark = Label(
- self._frm_advanced, text="", bg=self._bgcol, fg="grey", width=15, anchor=E
+ self._frm_advanced, bg=BGCOL, fg="deepskyblue", width=8, anchor=W
)
self._lbl_siv = Label(
- self._frm_advanced2, bg=self._bgcol, fg="yellow", width=2, anchor=W
+ self._frm_advanced2, bg=BGCOL, fg="yellow", width=2, anchor=W
)
self._lbl_sip = Label(
- self._frm_advanced2, bg=self._bgcol, fg="yellow", width=2, anchor=W
+ self._frm_advanced2, bg=BGCOL, fg="yellow", width=2, anchor=W
)
self._lbl_pdop = Label(
- self._frm_advanced2, bg=self._bgcol, fg="mediumpurple1", width=12, anchor=W
+ self._frm_advanced2, bg=BGCOL, fg="mediumpurple1", width=14, anchor=W
)
self._lbl_diffcorr = Label(
- self._frm_advanced2, bg=self._bgcol, fg="hotpink", width=4, anchor=W
+ self._frm_advanced2, bg=BGCOL, fg="hotpink", width=3, anchor=W
)
- self.option_add("*Font", self.__app.font_sm)
self._lbl_hvdop = Label(
- self._frm_advanced2, bg=self._bgcol, fg="mediumpurple1", width=9, anchor=W
+ self._frm_advanced2, bg=BGCOL, fg="mediumpurple1", width=11, anchor=W
)
self._lbl_hvacc = Label(
- self._frm_advanced2, bg=self._bgcol, fg="aquamarine2", width=9, anchor=W
+ self._frm_advanced2, bg=BGCOL, fg="aquamarine2", width=11, anchor=W
)
self._lbl_lacc_u = Label(
self._frm_advanced2,
- bg=self._bgcol,
+ bg=BGCOL,
fg="aquamarine2",
anchor=NW,
width=2,
)
self._lbl_diffstat = Label(
- self._frm_advanced2, bg=self._bgcol, fg="hotpink", width=25, anchor=W
+ self._frm_advanced2, bg=BGCOL, fg="hotpink", width=25, anchor=W
)
def _do_layout(self):
@@ -264,18 +248,17 @@ def _do_layout(self):
self._lbl_lat.grid(column=4, row=0, pady=0, padx=0, sticky=W)
self._lbl_llon.grid(column=5, row=0, pady=0, padx=0, sticky=W)
self._lbl_lon.grid(column=6, row=0, pady=0, padx=0, sticky=W)
- self._lbl_lalt.grid(column=7, row=0, pady=0, padx=0, sticky=W)
- self._lbl_alt.grid(column=8, row=0, pady=0, padx=0, sticky=W)
- self._lbl_lfix.grid(column=9, row=0, pady=0, padx=0, sticky=W)
- self._lbl_fix.grid(column=10, row=0, pady=0, padx=0, sticky=W)
-
- self._lbl_lhae.grid(column=0, row=0, pady=0, padx=0, sticky=W)
- self._lbl_hae.grid(column=1, row=0, pady=0, padx=0, sticky=W)
- self._lbl_lspd.grid(column=2, row=0, pady=0, padx=0, sticky=W)
- self._lbl_spd.grid(column=3, row=0, pady=0, padx=0, sticky=W)
- self._lbl_ltrk.grid(column=4, row=0, pady=0, padx=0, sticky=W)
- self._lbl_trk.grid(column=5, row=0, pady=0, padx=0, sticky=W)
- self._lbl_benchmark.grid(column=6, row=0, pady=0, padx=0, sticky=E)
+ self._lbl_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_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)
+ self._lbl_lhae.grid(column=2, row=0, pady=0, padx=0, sticky=W)
+ self._lbl_hae.grid(column=3, row=0, pady=0, padx=0, sticky=W)
+ self._lbl_lspd.grid(column=4, row=0, pady=0, padx=0, sticky=W)
+ self._lbl_spd.grid(column=5, row=0, pady=0, padx=0, sticky=W)
+ self._lbl_ltrk.grid(column=6, row=0, pady=0, padx=0, sticky=W)
+ self._lbl_trk.grid(column=7, row=0, pady=0, padx=0, sticky=W)
self._lbl_lsiv.grid(column=0, row=0, pady=0, padx=0, sticky=W)
self._lbl_siv.grid(column=1, row=0, pady=0, padx=0, sticky=W)
self._lbl_lsip.grid(column=2, row=0, pady=0, padx=0, sticky=W)
@@ -294,6 +277,13 @@ def _do_layout(self):
self._toggle_advanced()
+ def _attach_events(self):
+ """
+ Bind events to frame.
+ """
+
+ self.bind("", self._on_resize)
+
def _toggle_advanced(self):
"""
Toggle advanced banner frame on or off.
@@ -314,13 +304,6 @@ def _toggle_advanced(self):
self._frm_advanced2.grid_forget()
self._btn_toggle.config(image=self._img_expand)
- def _attach_events(self):
- """
- Bind events to frame.
- """
-
- self.bind("", self._on_resize)
-
def update_conn_status(self, status: int):
"""
Update connection status icon
@@ -398,13 +381,6 @@ def _update_time(self):
else:
self._lbl_time.config(text=f"{tim:%H:%M:%S.%f}")
- # display run time in s and process time in µs
- if self.__app.configuration.get("version_s") == "BENCHMARK":
- tim = time()
- self._lbl_benchmark.config(
- text=f"{tim-self.__app.starttime:.0f} s {self.__app.processtime/1000:.0f} µs"
- )
-
def _update_pos(self, pos_format, units):
"""
Update position.
@@ -419,7 +395,7 @@ def _update_pos(self, pos_format, units):
hae = self.__app.gnss_status.hae
self._lbl_llat.config(text="lat:")
self._lbl_llon.config(text="lon:")
- self._lbl_lalt.config(text="hmsl:")
+ self._lbl_lhmsl.config(text="hmsl:")
self._lbl_lhae.config(text="hae:")
alt_u = "ft" if units in (UI, UIK) else "m"
@@ -430,7 +406,7 @@ def _update_pos(self, pos_format, units):
lat, lon = (m2ft(x) for x in (lat, lon))
self._lbl_llat.config(text="X:")
self._lbl_llon.config(text="Y:")
- self._lbl_lalt.config(text="Z:")
+ self._lbl_lhmsl.config(text="Z:")
self._lbl_lat.config(text=f"{lat:.4f}")
self._lbl_lon.config(text=f"{lon:.4f}")
else:
@@ -446,12 +422,12 @@ def _update_pos(self, pos_format, units):
hae = m2ft(hae)
self._lbl_lat.config(text=f"{lat:{deg_f}}")
self._lbl_lon.config(text=f"{lon:{deg_f}}")
- self._lbl_alt.config(text=f"{alt:.4f} {alt_u}")
+ self._lbl_hmsl.config(text=f"{alt:.4f} {alt_u}")
self._lbl_hae.config(text=f"{hae:.4f} {alt_u}")
except (TypeError, ValueError):
self._lbl_lat.config(text=NA)
self._lbl_lon.config(text=NA)
- self._lbl_alt.config(text=NA)
+ self._lbl_hmsl.config(text=NA)
self._lbl_hae.config(text=NA)
def _update_track(self, units):
@@ -593,12 +569,10 @@ def _update_dgps(self, units):
if station in [None, "", 0]:
station = NA
if bl == NA:
- self._lbl_diffstat.config(
- text=f"age {age}\nstation {station} baseline {bl}"
- )
+ self._lbl_diffstat.config(text=f"age {age}\nid {station} - {bl}")
else:
self._lbl_diffstat.config(
- text=f"age {age}\nstation {station} baseline {bl:.2f} {bl_u}"
+ text=f"age {age}\nid {station} - {bl:.2f} {bl_u}"
)
else:
self._lbl_diffstat.config(text="")
@@ -608,32 +582,28 @@ def _set_fontsize(self):
Adjust font sizes according to frame size
"""
- w = self.width
- txt = 100
for ctl in (
self._lbl_status_preset,
self._lbl_time,
self._lbl_lat,
self._lbl_lon,
- self._lbl_alt,
+ self._lbl_hmsl,
self._lbl_hae,
self._lbl_spd,
self._lbl_trk,
- self._lbl_benchmark,
self._lbl_pdop,
self._lbl_fix,
self._lbl_sip,
self._lbl_siv,
self._lbl_diffcorr,
):
- fnt, _ = scale_font(w, 16, txt)
- ctl.config(font=fnt)
+ ctl.config(font=scale_font(self.width, FONTBASE, FONTSCALE)[0])
for ctl in (
self._lbl_ltime,
self._lbl_llat,
self._lbl_llon,
- self._lbl_lalt,
+ self._lbl_lhmsl,
self._lbl_lhae,
self._lbl_lspd,
self._lbl_ltrk,
@@ -644,16 +614,14 @@ def _set_fontsize(self):
self._lbl_lacc,
self._lbl_lcorr,
):
- fnt, _ = scale_font(w, 12, txt)
- ctl.config(font=fnt)
+ ctl.config(font=scale_font(self.width, FONTBASE - 2, FONTSCALE)[0])
for ctl in (
self._lbl_hvdop,
self._lbl_hvacc,
self._lbl_diffstat,
):
- fnt, _ = scale_font(w, 10, txt)
- ctl.config(font=fnt)
+ ctl.config(font=scale_font(self.width, FONTBASE - 4, FONTSCALE)[0])
def _on_resize(self, event): # pylint: disable=unused-argument
"""
diff --git a/src/pygpsclient/canvas_map.py b/src/pygpsclient/canvas_map.py
index c9f5842f..1efac313 100644
--- a/src/pygpsclient/canvas_map.py
+++ b/src/pygpsclient/canvas_map.py
@@ -39,6 +39,7 @@
from requests import ConnectionError as ConnError
from requests import ConnectTimeout, RequestException, get
+from pygpsclient.canvas_subclasses import create_circle # pylint: disable=unused-import
from pygpsclient.globals import (
BGCOL,
CUSTOM,
@@ -74,6 +75,7 @@
DLGGPXOOB,
MAPCONFIGERR,
MAPOPENERR,
+ MAPPERMERR,
NOCONN,
NOWEBMAPCONN,
NOWEBMAPFIX,
@@ -261,8 +263,8 @@ def open_offline_map(
"""
err = OUTOFBOUNDS.format("unknown")
+ mpath = None
try:
- mpath = None
if maptype == IMPORT:
mpath = mappath
self._bounds = bounds
@@ -281,6 +283,8 @@ def open_offline_map(
err = ""
except (ValueError, IndexError):
err = MAPCONFIGERR
+ except PermissionError:
+ err = MAPPERMERR.format(mpath.split("/")[-1])
except (FileNotFoundError, UnidentifiedImageError):
err = MAPOPENERR.format(mpath.split("/")[-1])
diff --git a/src/pygpsclient/canvas_subclasses.py b/src/pygpsclient/canvas_subclasses.py
index 649ab916..4d743bdc 100644
--- a/src/pygpsclient/canvas_subclasses.py
+++ b/src/pygpsclient/canvas_subclasses.py
@@ -1,12 +1,11 @@
"""
canvas_subclasses.py
-Multi-purpose CanvasContainer, CanvasGraph and CanvasCompass subclasses
-for PyGPSClient application.
+Various custom canvas methods and subclasses (see also canvas_map.py).
-Simplifies plotting of graphs and compass representations.
-
-(see also canvas_map.py)
+- CanvasContainer: scrollable and resizeable container for Toplevel dialogs
+- CanvasGraph: configurable x,y graph plotter
+- CanvasCompass: configurable compass plotter
Created on 20 Nov 2025
@@ -21,6 +20,7 @@
from math import ceil, cos, radians, sin
from tkinter import (
ALL,
+ CENTER,
EW,
HORIZONTAL,
NE,
@@ -40,17 +40,69 @@
)
from typing import Literal
-from pygpsclient.globals import GRIDLEGEND, GRIDMAJCOL, GRIDMINCOL, SQRT2, TIME0
+from pygpsclient.globals import GRIDLEGEND, GRIDMAJCOL, GRIDMINCOL, PNTCOL, SQRT2, TIME0
+from pygpsclient.helpers import fitfont
TAG_DATA = "dat"
TAG_GRID = "grd"
TAG_XLABEL = "xlb"
TAG_YLABEL = "ylb"
+TAG_WAIT = "wait"
MODE_CEL = "ele"
MODE_POL = "lin"
DEFRADII = {"ele": (0, 30, 45, 60, 75, 90), "lin": range(10, 1, -2)}
+def create_circle(self: Canvas, x: int, y: int, r: int, **kwargs):
+ """
+ Extends tkinter.Canvas class to simplify drawing compass grids on canvas.
+
+ :param int x: x coordinate
+ :param int y: y coordinate
+ :param int r: radius
+ """
+
+ return self.create_oval(x - r, y - r, x + r, y + r, **kwargs)
+
+
+Canvas.create_circle = create_circle
+
+
+def create_alert(
+ self: Canvas,
+ text: str,
+ fill: str = PNTCOL,
+ tags: str | list[str] | tuple[str] = TAG_DATA,
+ **kwargs,
+) -> int:
+ """
+ Extends tkinter.Canvas class to display alert at centre of canvas.
+
+ :param str text: message
+ :param str fill: fill color
+ :param str | list[str] | tuple[str] tags: tags
+ :return: create_text return code
+ :rtype: int
+ """
+
+ self.delete(tags)
+ w, h = self.winfo_width(), self.winfo_height()
+ fnt, _, _, _ = fitfont(text, w, h)
+ return self.create_text(
+ w / 2,
+ h / 2,
+ text=text,
+ font=fnt,
+ fill=fill,
+ anchor=CENTER,
+ tags=tags,
+ **kwargs,
+ )
+
+
+Canvas.create_alert = create_alert
+
+
class CanvasContainer(Canvas):
"""
Custom expandable and scrollable Canvas Container class,
@@ -58,21 +110,21 @@ class CanvasContainer(Canvas):
application window size.
"""
- def __init__(self, app, container, *args, **kwargs):
+ def __init__(self, app, parent, *args, **kwargs):
"""
Constructor.
:param app: Application
- :param container: Container frame
+ :param parent: Parent frame
"""
self.__app = app # Reference to main application class
self.__master = self.__app.appmaster # Reference to root class (Tk)
- self.x_scrollbar = Scrollbar(container, orient=HORIZONTAL)
- self.y_scrollbar = Scrollbar(container, orient=VERTICAL)
+ self.x_scrollbar = Scrollbar(parent, orient=HORIZONTAL)
+ self.y_scrollbar = Scrollbar(parent, orient=VERTICAL)
super().__init__(
- container,
+ parent,
xscrollcommand=self.x_scrollbar.set,
yscrollcommand=self.y_scrollbar.set,
*args,
@@ -87,8 +139,8 @@ def __init__(self, app, container, *args, **kwargs):
# 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)))
- container.grid_columnconfigure(0, weight=1)
- container.grid_rowconfigure(0, weight=1)
+ parent.grid_columnconfigure(0, weight=1)
+ parent.grid_rowconfigure(0, weight=1)
def show_scroll(self, show: bool = True):
"""
@@ -110,17 +162,17 @@ class CanvasGraph(Canvas):
Custom Canvas Graph class.
"""
- def __init__(self, app, container, *args, **kwargs):
+ def __init__(self, app, parent, *args, **kwargs):
"""
Constructor.
:param app: Application
- :param container: Container frame
+ :param parent: Parent frame
"""
self.__app = app # Reference to main application class
self.__master = self.__app.appmaster # Reference to root class (Tk)
- super().__init__(container, *args, **kwargs)
+ super().__init__(parent, *args, **kwargs)
def create_graph(
self,
@@ -448,7 +500,7 @@ class CanvasCompass(Canvas):
def __init__(
self,
app: object,
- container: Frame,
+ parent: Frame,
mode: Literal["ele", "lin"],
*args,
**kwargs,
@@ -457,7 +509,7 @@ def __init__(
Constructor.
:param app: Application
- :param container: Container frame
+ :param parent: Parent frame
:param int mode: ele (celestial) or lin (polar) coordinate system
"""
@@ -469,7 +521,7 @@ 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__(container, *args, **kwargs)
+ super().__init__(parent, *args, **kwargs)
def create_compass(
self,
diff --git a/src/pygpsclient/chart_frame.py b/src/pygpsclient/chart_frame.py
index 4b2b166f..8144f18e 100644
--- a/src/pygpsclient/chart_frame.py
+++ b/src/pygpsclient/chart_frame.py
@@ -424,9 +424,7 @@ def update_data(self, parsed_data: object):
continue
# wildcards *+-, sum, max or min of group of values
- if name == "processtime":
- val = self.__app.processtime / 1000 # microseconds
- elif name[-1] in ("*", "+", "-"):
+ if name[-1] in ("*", "+", "-"):
vals = []
for attr in parsed_data.__dict__:
if name[:-1] in attr and name[0] != "_":
diff --git a/src/pygpsclient/configuration.py b/src/pygpsclient/configuration.py
index 95a7666b..6bf600be 100644
--- a/src/pygpsclient/configuration.py
+++ b/src/pygpsclient/configuration.py
@@ -15,6 +15,7 @@
import logging
from os import getenv
from types import NoneType
+from typing import Any
from pygnssutils import (
PYGNSSUTILS_CRT,
@@ -115,6 +116,7 @@ def __init__(self, app):
"rtcmprot_b": 1,
"sbfprot_b": 0,
"qgcprot_b": 0,
+ "uniprot_b": 0,
"spartnprot_b": 0,
"mqttprot_b": 1,
"ttyprot_b": 0,
@@ -365,12 +367,12 @@ def loadcli(self, **kwargs):
if arg is not None:
self.set("tlscrtpath_s", arg)
- def set(self, name: str, value: object):
+ def set(self, name: str, value: Any):
"""
Set individual value.
:param str name: name of setting
- :param object value: value of setting
+ :param Any value: value of setting
:raises: KeyError if setting does not exist
"""
@@ -378,13 +380,13 @@ def set(self, name: str, value: object):
self.settings[name] = value
# self.logger.debug(f"{name=} {value=}")
- def get(self, name: str) -> object:
+ def get(self, name: str) -> Any:
"""
Get individual value.
:param str name: name of setting
:return: setting value
- :rtype: object
+ :rtype: Any
:raises: KeyError if setting does not exist
"""
diff --git a/src/pygpsclient/dynamic_config_frame.py b/src/pygpsclient/dynamic_config_frame.py
index 98a3d6c1..8a102ed7 100644
--- a/src/pygpsclient/dynamic_config_frame.py
+++ b/src/pygpsclient/dynamic_config_frame.py
@@ -528,6 +528,7 @@ def _clear_widgets(self):
wdgs = self._frm_attrs.grid_slaves()
for wdg in wdgs:
wdg.destroy()
+ wdg = None
Label(self._frm_attrs, text="Attribute", width=12, anchor=W).grid(
column=0, row=0, padx=3, sticky=(W)
)
diff --git a/src/pygpsclient/file_handler.py b/src/pygpsclient/file_handler.py
index 5f1e68bf..975848e5 100644
--- a/src/pygpsclient/file_handler.py
+++ b/src/pygpsclient/file_handler.py
@@ -40,7 +40,7 @@
HOME,
XML_HDR,
)
-from pygpsclient.helpers import set_filename
+from pygpsclient.helpers import set_filename, valid_geom
from pygpsclient.strings import CONFIGTITLE, GITHUB_URL, SAVETITLE
DEFEXT = ("all files", "*.*")
@@ -157,6 +157,9 @@ def validate_config(self, config: dict) -> str:
for key, value in config.items():
ctype = key[-2:]
valstr = f'"{value}"' if isinstance(value, str) else value
+ if key == "screengeom_s" and not valid_geom(value):
+ err = f"Invalid geometry format for {key}: {valstr}"
+ break
if ctype == "_n" and not isinstance(value, int):
err = f"Invalid int value for {key}: {valstr}"
break
diff --git a/src/pygpsclient/globals.py b/src/pygpsclient/globals.py
index db2194bb..f6c96df1 100644
--- a/src/pygpsclient/globals.py
+++ b/src/pygpsclient/globals.py
@@ -18,7 +18,6 @@
from datetime import datetime
from os import path
from pathlib import Path
-from tkinter import Canvas
from pyubx2 import GET, POLL, SET, SETPOLL
@@ -30,21 +29,6 @@
PointXY = namedtuple("PointXY", ["x", "y"])
AreaXY = namedtuple("AreaXY", ["x1", "y1", "x2", "y2"])
-
-def create_circle(self: Canvas, x: int, y: int, r: int, **kwargs):
- """
- Extends tkinter.Canvas class to simplify drawing compass grids on canvas.
-
- :param int x: x coordinate
- :param int y: y coordinate
- :param int r: radius
- """
-
- return self.create_oval(x - r, y - r, x + r, y + r, **kwargs)
-
-
-Canvas.create_circle = create_circle
-
# Default colors
AXISCOL = "#E5E5E5" # default plot axis color
BGCOL = "#3D3D3D" # default widget background color
@@ -64,14 +48,15 @@ def create_circle(self: Canvas, x: int, y: int, r: int, **kwargs):
GRIDMAJCOL = "#666666" # default grid major tick color
GRIDMINCOL = "#4D4D4D" # default grid minor tick color
INFOCOL = "#5CACEE" # default info message color
-OKCOL = "#008000" # default OK message color
+OKCOL = "#02B102" # default OK message color
PLOTCOLS = ("#FFFF00", "#00FFFF", "#FF00FF", "#00BFFF")
PNTCOL = "#FF8000" # default plot point color
-# Protocols to be used in protocol mask (others defined in gnss_reader.py)
-SPARTN_PROTOCOL = 32
-MQTT_PROTOCOL = 64
-TTY_PROTOCOL = 128
+# Protocols to be used in protocol mask (others defined in pygnssutils.gnss_reader.py)
+UNI_PROTOCOL = 32 # provisional - awaiting pygnssutils.gnssreader updates for Unicore
+SPARTN_PROTOCOL = 256
+MQTT_PROTOCOL = 512
+TTY_PROTOCOL = 1024
# Various global constants - please keep in ascending alphabetical order
HOME = Path.home()
@@ -211,6 +196,7 @@ def create_circle(self: Canvas, x: int, y: int, r: int, **kwargs):
MAX_SNR = 60 # upper limit of levelsview CNo axis
MAXFLOAT = 2e20
MAXLOGSIZE = 10485760 # maximum size of individual log file in bytes
+MAXWAIT = 10 # maximum number of update cycles while waiting for data
MIN_GUI_UPDATE_INTERVAL = 0.1 # minimum GUI widget update interval (seconds)
MINFLOAT = -MAXFLOAT
MINDIM = "mindim"
@@ -270,6 +256,7 @@ def create_circle(self: Canvas, x: int, y: int, r: int, **kwargs):
TTYMARKER = "TTY<<"
UBXPRESETS = "ubxpresets"
UBXSIMULATOR = "ubxsimulator"
+UM980 = "Unicore UM98n"
UNDO = "UNDO"
UTF8 = "utf-8"
VALBLANK = 1
diff --git a/src/pygpsclient/gnss_status.py b/src/pygpsclient/gnss_status.py
index b6a1352e..4e29d6c3 100644
--- a/src/pygpsclient/gnss_status.py
+++ b/src/pygpsclient/gnss_status.py
@@ -28,6 +28,13 @@ def __init__(self):
Constructor.
"""
+ self.reset()
+
+ def reset(self):
+ """
+ Reset all data.
+ """
+
self.utc = datetime.now(timezone.utc).time().replace(microsecond=0) # UTC time
self.lat = 0.0 # latitude as decimal
self.lon = 0.0 # longitude as decimal
diff --git a/src/pygpsclient/gpx_dialog.py b/src/pygpsclient/gpx_dialog.py
index 820b6c40..b4944376 100644
--- a/src/pygpsclient/gpx_dialog.py
+++ b/src/pygpsclient/gpx_dialog.py
@@ -135,11 +135,9 @@ def _body(self):
"""
self._frm_body = Frame(self.container)
- self._frm_map = Frame(self._frm_body, borderwidth=2, relief="groove", bg=BGCOL)
- self._frm_profile = Frame(
- self._frm_body, borderwidth=2, relief="groove", bg=BGCOL
- )
- self._frm_info = Frame(self._frm_body, borderwidth=2, relief="groove", bg=BGCOL)
+ self._frm_map = Frame(self._frm_body, bg=BGCOL)
+ self._frm_profile = Frame(self._frm_body, bg=BGCOL)
+ self._frm_info = Frame(self._frm_body, bg=BGCOL)
self._frm_controls = Frame(self._frm_body, borderwidth=2, relief="groove")
self._can_mapview = CanvasMap(
self.__app,
diff --git a/src/pygpsclient/helpers.py b/src/pygpsclient/helpers.py
index bfd983db..2ce8baff 100644
--- a/src/pygpsclient/helpers.py
+++ b/src/pygpsclient/helpers.py
@@ -11,6 +11,7 @@
"""
+import re
from datetime import datetime, timedelta
from math import (
asin,
@@ -37,7 +38,8 @@
Tk,
font,
)
-from typing import Literal
+from types import NoneType
+from typing import Any, Literal
from pygnssutils import version as PGVERSION
from pynmeagps import WGS84_SMAJ_AXIS, NMEAMessage, haversine
@@ -421,7 +423,7 @@ def fitfont(
angle: int = 0,
maxsiz: int = 10,
constraint: int = 3,
-) -> tuple[font.Font, float, float]:
+) -> tuple[font.Font, float, float, int]:
"""
Create font to fit space.
@@ -431,8 +433,8 @@ def fitfont(
:param int angle: font angle in degrees
:param int maxsiz: maximum font size in pixels
:param int constraint: 1 = width, 2 = height, 3 = width & height
- :return: tuple of (sized font, font width, font height)
- :rtype: tuple[font.Font, float, float]
+ :return: tuple of (sized font, font width, font height, font size in pixels)
+ :rtype: tuple[font.Font, float, float, int]
"""
fw, fh = maxw + 1, maxh + 1
@@ -445,7 +447,7 @@ def fitfont(
fnt = font.Font(size=-siz)
rw, rh = fontdim(fmt, fnt, angle)
siz -= 1
- return fnt, fw, fh
+ return fnt, fw, fh, siz
def fix2desc(msgid: str, fix: object) -> str:
@@ -780,14 +782,15 @@ def ll2xy(width: int, height: int, bounds: Area, position: Point) -> tuple:
return x, y
-def makeval(val: object, default: object = 0.0) -> object:
+def makeval(val: Any, default: Any = 0.0) -> Any:
"""
- Force value to be same type as default.
+ Force value to be same type as default
+ (used in sqlite database insertions).
:param object val: value
- :param object default: default value
+ :param Any default: default value
:return: value or default
- :rtype: object
+ :rtype: Any
"""
if (not isinstance(val, type(default))) or (
@@ -1426,6 +1429,22 @@ def val2sphp(val: float, scale: float) -> tuple:
return val_sp, val_hp
+def valid_geom(geom: str) -> bool:
+ """
+ Validate tkinter screen geometry string "%Wx%H+%X+%Y".
+ %X,%Y can be negative on multiple display configurations.
+
+ :param str geom: screen geom
+ :return: Valid/Invalid
+ :rtype: bool
+ """
+
+ if geom == "":
+ return True
+ regexgeom = re.compile(r"^([1-9][0-9]*)[x]([1-9][0-9]*)[+]-?\d*[+]-?\d*$")
+ return regexgeom.match(geom) is not None
+
+
def wnotow2date(wno: int, tow: int) -> datetime:
"""
Get datetime from GPS Week number (Wno) and Time of Week (Tow).
@@ -1443,7 +1462,7 @@ def wnotow2date(wno: int, tow: int) -> datetime:
return dat
-def xy2ll(width: int, height: int, bounds: Area, xy: tuple) -> Point:
+def xy2ll(width: int, height: int, bounds: Area, xy: tuple) -> Point | NoneType:
"""
Convert canvas x/y to lat/lon.
@@ -1452,7 +1471,7 @@ def xy2ll(width: int, height: int, bounds: Area, xy: tuple) -> Point:
:param Area bounds: lat/lon bounds of canvas
:param tuple xy: canvas x/y coordinate
:return: lat/lon
- :rtype: Point
+ :rtype: Point | NoneType
"""
lw = bounds.lon2 - bounds.lon1
diff --git a/src/pygpsclient/imu_frame.py b/src/pygpsclient/imu_frame.py
index 532d526f..3a55b0ea 100644
--- a/src/pygpsclient/imu_frame.py
+++ b/src/pygpsclient/imu_frame.py
@@ -30,6 +30,7 @@
from pynmeagps import FMI_STATUS
from pyubx2 import ESFALG_STATUS
+from pygpsclient.canvas_subclasses import TAG_WAIT
from pygpsclient.globals import (
BGCOL,
ERRCOL,
@@ -42,7 +43,7 @@
WIDGETU2,
)
from pygpsclient.helpers import rgb2str, scale_font
-from pygpsclient.strings import LBLNODATA
+from pygpsclient.strings import DLGWAITIMU
OFFSETX = 5
OFFSETY = 10
@@ -95,6 +96,7 @@ def __init__(self, app: Frame, parent: Frame, *args, **kwargs):
self._range = IntVar()
self._option = StringVar()
self._range.set(RANGES[0])
+ self._waiting = True
self._body()
self.reset()
self._attach_events()
@@ -107,7 +109,7 @@ def _body(self):
self.grid_rowconfigure(0, weight=1)
self.grid_rowconfigure(1, weight=0)
self.grid_rowconfigure(2, weight=0)
- self.canvas = Canvas(self, width=self.width, height=self.height, bg=BGCOL)
+ self._canvas = Canvas(self, width=self.width, height=self.height, bg=BGCOL)
self._lbl_range = Label(self, text="Range", fg=FGCOL, bg=BGCOL, anchor=W)
self._spn_range = Spinbox(
self,
@@ -134,7 +136,7 @@ def _body(self):
textvariable=self._option,
state=READONLY,
)
- self.canvas.grid(column=0, row=0, columnspan=4, sticky=NSEW)
+ self._canvas.grid(column=0, row=0, columnspan=4, sticky=NSEW)
self._lbl_range.grid(column=0, row=1, sticky=EW)
self._spn_range.grid(column=1, row=1, sticky=EW)
@@ -156,7 +158,7 @@ def _attach_events(self):
"""
self.bind("", self._on_resize)
- self.canvas.bind("", self._on_clear)
+ self._canvas.bind("", self._on_clear)
for setting in (self._range, self._option):
setting.trace_add(TRACEMODE_WRITE, self._on_update_config)
@@ -182,8 +184,8 @@ def init_frame(self):
width, _ = self.get_size()
self._flag_range(False)
- self.canvas.delete("all")
- self.canvas.create_line(
+ self._canvas.delete("all")
+ self._canvas.create_line(
(width - OFFSETX * 2) / 2,
OFFSETY,
(width - OFFSETX * 2) / 2,
@@ -191,7 +193,7 @@ def init_frame(self):
fill=GRIDMAJCOL,
width=2,
)
- self.canvas.create_text(
+ self._canvas.create_text(
OFFSETX,
OFFSETY,
text="Roll X:",
@@ -199,7 +201,7 @@ def init_frame(self):
font=self._font,
anchor=NW,
)
- self.canvas.create_text(
+ self._canvas.create_text(
OFFSETX,
self._fonth * 2 + OFFSETY,
text="Pitch Y:",
@@ -207,7 +209,7 @@ def init_frame(self):
font=self._font,
anchor=NW,
)
- self.canvas.create_text(
+ self._canvas.create_text(
OFFSETX,
self._fonth * 4 + OFFSETY,
text="Yaw Z:",
@@ -215,7 +217,7 @@ def init_frame(self):
font=self._font,
anchor=NW,
)
- self.canvas.create_text(
+ self._canvas.create_text(
OFFSETX,
self._fonth * 6 + OFFSETY,
text="Source:",
@@ -223,7 +225,7 @@ def init_frame(self):
font=self._font,
anchor=NW,
)
- self.canvas.create_text(
+ self._canvas.create_text(
OFFSETX,
self._fonth * 7 + OFFSETY,
text="Status:",
@@ -236,8 +238,8 @@ def update_frame(self):
"""
Collect scatterplot data and update the plot.
"""
- self.canvas.delete(DATA)
- width, height = self.get_size()
+ self._canvas.delete(DATA)
+ width, _ = self.get_size()
owidth = width - OFFSETX * 2
wid = int(self._fonth / 2)
wid2 = int(wid / 2)
@@ -245,6 +247,9 @@ def update_frame(self):
try:
scale = self._range.get()
data = self.__app.gnss_status.imu_data
+ if data != {}:
+ self._waiting = False
+ self._canvas.delete(TAG_WAIT)
rng = self._range.get()
roll = float(data["roll"])
pitch = float(data["pitch"])
@@ -272,7 +277,7 @@ def update_frame(self):
if abs(roll) > scale or abs(pitch) > scale or abs(yaw) > scale:
self._flag_range(True)
- self.canvas.create_text(
+ self._canvas.create_text(
OFFSETX,
OFFSETY,
text=f"\t{roll}",
@@ -281,7 +286,7 @@ def update_frame(self):
anchor=NW,
tag=DATA,
)
- self.canvas.create_line(
+ self._canvas.create_line(
owidth / 2,
self._fonth + OFFSETY + wid2,
owidth / 2 + rollbar,
@@ -290,7 +295,7 @@ def update_frame(self):
width=wid,
tag=DATA,
)
- self.canvas.create_text(
+ self._canvas.create_text(
OFFSETX,
self._fonth * 2 + OFFSETY,
text=f"\t{pitch}",
@@ -299,7 +304,7 @@ def update_frame(self):
anchor=NW,
tag=DATA,
)
- self.canvas.create_line(
+ self._canvas.create_line(
owidth / 2,
self._fonth * 3 + OFFSETY + wid2,
owidth / 2 + pitchbar,
@@ -308,7 +313,7 @@ def update_frame(self):
width=wid,
tag=DATA,
)
- self.canvas.create_text(
+ self._canvas.create_text(
OFFSETX,
self._fonth * 4 + OFFSETY,
text=f"\t{yaw}",
@@ -317,7 +322,7 @@ def update_frame(self):
anchor=NW,
tag=DATA,
)
- self.canvas.create_line(
+ self._canvas.create_line(
owidth / 2,
self._fonth * 5 + OFFSETY + wid2,
owidth / 2 + yawbar,
@@ -326,7 +331,7 @@ def update_frame(self):
width=wid,
tag=DATA,
)
- self.canvas.create_text(
+ self._canvas.create_text(
OFFSETX,
self._fonth * 6 + OFFSETY,
text=f"\t{source}",
@@ -335,7 +340,7 @@ def update_frame(self):
anchor=NW,
tag=DATA,
)
- self.canvas.create_text(
+ self._canvas.create_text(
OFFSETX,
self._fonth * 7 + OFFSETY,
text=f"{status}",
@@ -346,11 +351,8 @@ def update_frame(self):
)
except (KeyError, ValueError):
- self.canvas.delete(DATA)
- self.canvas.create_text(
- width / 2, height / 2, text=LBLNODATA, fill=ERRCOL, tag=DATA
- )
- self.canvas.update_idletasks()
+ self._canvas.delete(DATA)
+ self._canvas.update_idletasks()
def _flag_range(self, over: bool = False):
"""
@@ -393,6 +395,15 @@ def _on_resize(self, event): # pylint: disable=unused-argument
self._font, self._fonth = scale_font(self.width, 12, 25, 30)
self.init_frame()
self._redraw()
+ self._on_waiting()
+
+ def _on_waiting(self):
+ """
+ Display 'waiting for data' alert.
+ """
+
+ if self._waiting:
+ self._canvas.create_alert(DLGWAITIMU, tags=TAG_WAIT)
def get_size(self) -> tuple:
"""
@@ -403,4 +414,4 @@ def get_size(self) -> tuple:
"""
self.update_idletasks() # Make sure we know about resizing
- return self.canvas.winfo_width(), self.canvas.winfo_height()
+ return self._canvas.winfo_width(), self._canvas.winfo_height()
diff --git a/src/pygpsclient/init_presets.py b/src/pygpsclient/init_presets.py
index 1c00bd7a..9b6d0624 100644
--- a/src/pygpsclient/init_presets.py
+++ b/src/pygpsclient/init_presets.py
@@ -97,6 +97,10 @@
"Septentrio X5 Set Fixed Base Station Mode; setDataInOut,COM1, ,RTCMv3;setRTCMv3Formatting,1234;setStaticPosGeodetic,Geodetic1,37.23345,-115.81513,15;setPVTMode,Static, ,Geodetic1",
"Septentrio X5 Set Survey-In Base Station Mode;setDataInOut,COM1, ,RTCMv3;setRTCMv3Formatting,1234;setPVTMode,Static, ,auto",
"Septentrio X5 Stop RTCM output;setDataInOut,COM1, ,none",
+ "Unicore UM98n Restore Factory Defaults CONFIRM; freset",
+ "Unicore UM98n Save Current Configuration to NVM CONFIRM; saveconfig",
+ "Unicore UM98n Stop All Output on COM1; unlog COM1",
+ "Unicore UM98n Stop All Output on COM2; unlog COM2",
"Feyman IM19 Tilt Survey Setup; AT+LOAD_DEFAULT; AT+GNSS_PORT=PHYSICAL_UART2; AT+NASC_OUTPUT=UART1,ON; AT+LEVER_ARM2=0.0057,-0.0732,-0.0645; AT+CLUB_VECTOR=0,0,1.865; AT+INSTALL_ANGLE=0,180,0; AT+GNSS_CARD=OEM; AT+WORK_MODE=408; AT+CORRECT_HOLDER=ENABLE; AT+SET_PPS_EDGE=RISING; AT+AHRS=ENABLE; AT+MAG_AUTO_SAVE=ENABLE; AT+SAVE_ALL",
"Feyman IM19 System reset CONFIRM; AT+SYSTEM_RESET",
"Feyman IM19 Save the parameters CONFIRM; AT+SAVE_ALL",
diff --git a/src/pygpsclient/levelsview_frame.py b/src/pygpsclient/levelsview_frame.py
index c7f24f76..acd81b6f 100644
--- a/src/pygpsclient/levelsview_frame.py
+++ b/src/pygpsclient/levelsview_frame.py
@@ -19,6 +19,7 @@
from pygpsclient.canvas_subclasses import (
TAG_DATA,
TAG_GRID,
+ TAG_WAIT,
TAG_XLABEL,
TAG_YLABEL,
CanvasGraph,
@@ -32,6 +33,7 @@
WIDGETU2,
)
from pygpsclient.helpers import col2contrast, fitfont, unused_sats
+from pygpsclient.strings import DLGWAITCNO
OL_WID = 1
FONTSCALELG = 40
@@ -63,6 +65,7 @@ def __init__(self, app: Frame, parent: Frame, *args, **kwargs):
self.width = kwargs.get("width", def_w)
self.height = kwargs.get("height", def_h)
self._redraw = True
+ self._waiting = True
self._body()
self._attach_events()
@@ -118,7 +121,7 @@ def init_frame(self):
"""
# only redraw the tags that have changed
- tags = (TAG_GRID, TAG_XLABEL, TAG_YLABEL) if self._redraw else ()
+ tags = (TAG_GRID, TAG_XLABEL, TAG_YLABEL, TAG_WAIT) if self._redraw else ()
self._canvas.create_graph(
xdatamax=10,
ydatamax=(MAX_SNR,),
@@ -177,12 +180,13 @@ def update_frame(self):
if siv <= 0:
return
+ self._waiting = False
w, h = self.width, self.height
self.init_frame()
offset = self._canvas.xoffl
colwidth = (w - self._canvas.xoffl - self._canvas.xoffr + 1) / siv
- xfnt, _, _ = fitfont(XLBLFMT, colwidth, self._canvas.yoffb, XLBLANGLE)
+ 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:
@@ -224,6 +228,15 @@ def _on_resize(self, event): # pylint: disable=unused-argument
self.width, self.height = self.get_size()
self._redraw = True
+ self._on_waiting()
+
+ def _on_waiting(self):
+ """
+ Display 'waiting for data' alert.
+ """
+
+ if self._waiting:
+ self._canvas.create_alert(DLGWAITCNO, tags=TAG_WAIT)
def get_size(self):
"""
diff --git a/src/pygpsclient/map_frame.py b/src/pygpsclient/map_frame.py
index 9e67adaf..0a664539 100644
--- a/src/pygpsclient/map_frame.py
+++ b/src/pygpsclient/map_frame.py
@@ -37,6 +37,7 @@
)
from pygpsclient.canvas_map import HYB, MAP, MAPTYPES, SAT, TAG_CLOCK, CanvasMap
+from pygpsclient.canvas_subclasses import TAG_WAIT
from pygpsclient.globals import (
BGCOL,
ERRCOL,
@@ -52,7 +53,7 @@
MIN_UPDATE_INTERVAL,
MIN_ZOOM,
)
-from pygpsclient.strings import LBLSHOWTRACK, NOWEBMAPFIX
+from pygpsclient.strings import DLGWAITPOS, LBLSHOWTRACK
ZOOMCOL = ERRCOL
ZOOMEND = "lightgray"
@@ -85,10 +86,11 @@ def __init__(self, app: Frame, parent: Frame, *args, **kwargs):
self.width = kwargs.get("width", def_w)
self.height = kwargs.get("height", def_h)
self._img = None
- self._last_map_update = 0
+ self.__app.last_map_update = 0
self._lastmaptype = ""
self._lastmappath = ""
self._bounds = None
+ self._waiting = True
self._maptype = StringVar()
self._maptype.set(self.__app.configuration.get("maptype_s"))
self._showtrack = IntVar()
@@ -105,7 +107,7 @@ def _body(self):
Set up frame and widgets.
"""
- self._can_mapview = CanvasMap(
+ self._canvas = CanvasMap(
self.__app,
self,
height=self.height,
@@ -156,7 +158,7 @@ def _do_layout(self):
Arrange widgets in frame.
"""
- self._can_mapview.grid(column=0, row=0, sticky=NSEW)
+ self._canvas.grid(column=0, row=0, sticky=NSEW)
self._frm_options.grid(column=0, row=1, sticky=EW)
self._spn_maptype.grid(column=0, row=0, padx=1, sticky=W)
self._lbl_zoom.grid(column=1, row=0, padx=1, sticky=W)
@@ -164,8 +166,8 @@ def _do_layout(self):
self._chk_showtrack.grid(column=3, row=0, padx=1, sticky=W)
self.grid_columnconfigure(0, weight=1)
self.grid_rowconfigure(0, weight=1)
- self._can_mapview.grid_columnconfigure(0, weight=1)
- self._can_mapview.grid_rowconfigure(0, weight=1)
+ self._canvas.grid_columnconfigure(0, weight=1)
+ self._canvas.grid_rowconfigure(0, weight=1)
def _attach_events(self):
"""
@@ -176,7 +178,7 @@ def _attach_events(self):
self._maptype.trace_add(TRACEMODE_WRITE, self.on_maptype)
self._mapzoom.trace_add(TRACEMODE_WRITE, self.on_mapzoom)
self._showtrack.trace_add(TRACEMODE_WRITE, self.on_showtrack)
- self._can_mapview.bind("", self.on_refresh)
+ self._canvas.bind("", self.on_refresh)
self._lbl_zoom.bind(
"",
lambda event, val=0: self._setzoom(val),
@@ -207,7 +209,7 @@ def on_refresh(self, event): # pylint: disable=unused-argument
:param event: event
"""
- self._last_map_update = 0
+ self.__app.last_map_update = 0
def on_maptype(self, var, index, mode): # pylint: disable=unused-argument
"""
@@ -254,7 +256,7 @@ def on_showtrack(self, var, index, mode): # pylint: disable=unused-argument
self.__app.configuration.set("showtrack_b", self._showtrack.get())
if not self._showtrack.get():
- self._can_mapview.track = None
+ self._canvas.track = None
self.on_refresh(None)
def update_frame(self):
@@ -272,56 +274,43 @@ def update_frame(self):
self._maptype.set(self.__app.configuration.get("maptype_s"))
# if no valid position, display warning message
- # fix = kwargs.get("fix", 0)
- if (
- lat in (None, "")
- or lon in (None, "")
- or (lat == 0 and lon == 0)
- # or fix in (0, 5) # no fix or time only
- ):
- self.reset_map_refresh()
- self._can_mapview.draw_msg(NOWEBMAPFIX)
+ if lat in (None, "") or lon in (None, "") or (lat == 0 and lon == 0):
+ self.__app.last_map_update = 0
return
+ self._waiting = False
# record track if Show Track checkbox ticked
if self._showtrack.get():
- self._can_mapview.track = Point(lat, lon)
+ self._canvas.track = Point(lat, lon)
# limit mapquest calls to specified interval to avoid cost
maptype = self._maptype.get()
if maptype in (MAP, SAT, HYB):
now = time()
- if now - self._last_map_update < map_update_interval:
- self._can_mapview.draw_countdown(
- (-360 / map_update_interval) * (now - self._last_map_update)
+ if now - self.__app.last_map_update < map_update_interval:
+ self._canvas.draw_countdown(
+ (-360 / map_update_interval) * (now - self.__app.last_map_update)
)
return
- self._last_map_update = now
+ self.__app.last_map_update = now
else:
- self._can_mapview.delete(TAG_CLOCK)
+ self._canvas.delete(TAG_CLOCK)
- self._can_mapview.draw_map(
+ self._canvas.draw_map(
maptype=self._maptype.get(),
location=Point(lat, lon),
- track=self._can_mapview.track,
- marker=self._can_mapview.marker,
+ track=self._canvas.track,
+ marker=self._canvas.marker,
hacc=hacc,
zoom=self._mapzoom.get(),
)
- self._bounds = self._can_mapview.bounds
+ self._bounds = self._canvas.bounds
- if self._can_mapview.zoommin:
+ if self._canvas.zoommin:
self._spn_zoom.config(highlightbackground=ERRCOL, highlightthickness=3)
else:
self._spn_zoom.config(highlightbackground="gray90", highlightthickness=3)
- def reset_map_refresh(self):
- """
- Reset map refresh counter to zero
- """
-
- self._last_map_update = 0
-
def _on_resize(self, event): # pylint: disable=unused-argument
"""
Resize frame
@@ -330,6 +319,15 @@ def _on_resize(self, event): # pylint: disable=unused-argument
"""
self.width, self.height = self.get_size()
+ self._on_waiting()
+
+ def _on_waiting(self):
+ """
+ Display 'waiting for data' alert.
+ """
+
+ if self._waiting:
+ self._canvas.create_alert(DLGWAITPOS, tags=TAG_WAIT)
def get_size(self):
"""
@@ -340,4 +338,4 @@ def get_size(self):
"""
self.update_idletasks() # Make sure we know about any resizing
- return self._can_mapview.winfo_width(), self._can_mapview.winfo_height()
+ return self._canvas.winfo_width(), self._canvas.winfo_height()
diff --git a/src/pygpsclient/nmea_config_dialog.py b/src/pygpsclient/nmea_config_dialog.py
index 85bf79fd..4c2bac55 100644
--- a/src/pygpsclient/nmea_config_dialog.py
+++ b/src/pygpsclient/nmea_config_dialog.py
@@ -68,11 +68,9 @@ def _body(self):
"""
# add configuration widgets
- self._frm_device_info = Hardware_Info_Frame(
- self.__app, self, borderwidth=2, relief="groove", protocol="NMEA"
- )
+ self._frm_device_info = Hardware_Info_Frame(self.__app, self, protocol="NMEA")
self._frm_config_dynamic = Dynamic_Config_Frame(
- self.__app, self, borderwidth=2, relief="groove", protocol="NMEA"
+ self.__app, self, protocol="NMEA"
)
self._frm_preset = NMEA_PRESET_Frame(
self.__app,
diff --git a/src/pygpsclient/nmea_handler.py b/src/pygpsclient/nmea_handler.py
index 79bc9713..d6e33cb5 100644
--- a/src/pygpsclient/nmea_handler.py
+++ b/src/pygpsclient/nmea_handler.py
@@ -47,7 +47,7 @@ def __init__(self, app):
self._raw_data = None
self._parsed_data = None
- def process_data(self, raw_data: bytes, parsed_data: object):
+ def process_data(self, raw_data: bytes, parsed_data: NMEAMessage):
"""
Process relevant NMEA message types
@@ -60,19 +60,28 @@ def process_data(self, raw_data: bytes, parsed_data: object):
if raw_data is None:
return
# self.logger.debug(f"data received {parsed_data.identity}")
- if parsed_data.msgID == "RMC": # Recommended minimum data for GPS
+ if parsed_data.msgID in ("RMC", "RMCH"): # Recommended minimum data for GPS
self._process_RMC(parsed_data)
- elif parsed_data.msgID == "GGA": # GPS Fix Data
+ elif parsed_data.msgID in (
+ "GGA",
+ "GGAH",
+ ): # GPS Fix Data (H = Unicore Extended)
self._process_GGA(parsed_data)
- elif parsed_data.msgID == "GLL": # GPS Lat Lon Data
+ elif parsed_data.msgID in ("GLL", "GLLH"): # GPS Lat Lon Data
self._process_GLL(parsed_data)
- elif parsed_data.msgID == "GNS": # GNSS Fix Data
+ elif parsed_data.msgID in ("GNS", "GNSH"): # GNSS Fix Data
self._process_GNS(parsed_data)
- elif parsed_data.msgID == "GSA": # GPS DOP (Dilution of Precision)
+ elif parsed_data.msgID in (
+ "GSA",
+ "GSAH",
+ ): # GPS DOP (Dilution of Precision)
self._process_GSA(parsed_data)
- elif parsed_data.msgID == "VTG": # GPS Vector track and Speed over Ground
+ elif parsed_data.msgID in (
+ "VTG",
+ "VTGH",
+ ): # GPS Vector track and Speed over Ground
self._process_VTG(parsed_data)
- elif parsed_data.msgID == "GSV": # GPS Satellites in View
+ elif parsed_data.msgID in ("GSV", "GSVH"): # GPS Satellites in View
self._process_GSV(parsed_data)
elif parsed_data.msgID == "ZDA": # ZDA Time
self._process_ZDA(parsed_data)
@@ -91,6 +100,11 @@ def process_data(self, raw_data: bytes, parsed_data: object):
elif parsed_data.msgID == "QTMSVINSTATUS": # LG290P SVIN status
self._process_QTMSVINSTATUS(parsed_data)
elif parsed_data.msgID in (
+ "HPR",
+ "HPR2",
+ "HPD",
+ "TRA",
+ "TRA2",
"QTMDRPVA",
"QTMINS",
"QTMVEHATT",
@@ -430,8 +444,7 @@ def _process_FMI(self, data: NMEAMessage):
def _process_IMU(self, data: NMEAMessage):
"""
- Process IMU status from various proprietary NMEA sentences:
- PQTMINS, PQTMDRPVA, PQTMVEHATT, PQTMTAR, PINVMATTIT, PSTMDRPVA
+ Process IMU status from various proprietary NMEA sentences.
Roll, Pitch and Yaw (Heading) are in degrees.
@@ -441,12 +454,11 @@ def _process_IMU(self, data: NMEAMessage):
try:
ims = self.__app.gnss_status.imu_data
ims["source"] = data.identity
- ims["roll"] = round(data.roll, 4)
- ims["pitch"] = round(data.pitch, 4)
- if hasattr(data, "yaw"):
- ims["yaw"] = round(data.yaw, 4)
- elif hasattr(data, "heading"):
- ims["yaw"] = round(data.heading, 4)
- ims["status"] = ""
+ ims["roll"] = round(getattr(data, "roll", 0), 4)
+ ims["pitch"] = round(getattr(data, "pitch", 0), 4)
+ ims["yaw"] = round(getattr(data, "yaw", 0), 4)
+ if hasattr(data, "heading"): # range 0 - 360
+ ims["yaw"] = round(data.heading - 180, 4)
+ ims["status"] = str(getattr(data, "quality", ""))
except (TypeError, KeyError, AttributeError):
pass
diff --git a/src/pygpsclient/qgc_handler.py b/src/pygpsclient/qgc_handler.py
index 61cb8bd1..16b7a957 100644
--- a/src/pygpsclient/qgc_handler.py
+++ b/src/pygpsclient/qgc_handler.py
@@ -37,6 +37,7 @@ def __init__(self, app):
self._raw_data = None
self._parsed_data = None
+ # pylint: disable=unused-argument
def process_data(self, raw_data: bytes, parsed_data: object):
"""
Process relevant QGC message types
diff --git a/src/pygpsclient/receiver_config_handler.py b/src/pygpsclient/receiver_config_handler.py
index b7612a80..520f8f69 100644
--- a/src/pygpsclient/receiver_config_handler.py
+++ b/src/pygpsclient/receiver_config_handler.py
@@ -108,6 +108,26 @@ def config_disable_septentrio() -> list:
return msgs
+def config_disable_unicore() -> list:
+ """
+ Disable base station mode for Unicore receivers.
+
+ :return: ASCII TTY commands
+ :rtype: list
+ """
+
+ msgs = []
+ msgs.append("mode rover survey\r\n")
+ msgs.append("rtcm1006 com1 0\r\n")
+ msgs.append("rtcm1033 com1 0\r\n")
+ msgs.append("rtcm1074 com1 0\r\n")
+ msgs.append("rtcm1084 com1 0\r\n")
+ msgs.append("rtcm1094 com1 0\r\n")
+ msgs.append("rtcm1124 com1 0\r\n")
+ msgs.append("saveconfig\r\n")
+ return msgs
+
+
def config_svin_ublox(acc_limit: int, svin_min_dur: int) -> UBXMessage:
"""
Configure Survey-In mode with specified accuracy limit for u-blox receivers.
@@ -219,6 +239,28 @@ def config_svin_septentrio(acc_limit: int, svin_min_dur: int) -> list:
return msgs
+def config_svin_unicore(acc_limit: int, svin_min_dur: int) -> list:
+ """
+ Configure Survey-In mode with specified accuracy limit for Unicore receivers.
+
+ :param int acc_limit: accuracy limit in cm
+ :param int svin_min_dur: survey minimimum duration
+ :return: ASCII TTY commands
+ :rtype: list
+ """
+
+ msgs = []
+ msgs.append(f"mode base time {svin_min_dur}\r\n")
+ msgs.append("rtcm1006 com1 10\r\n")
+ msgs.append("rtcm1033 com1 10\r\n")
+ msgs.append("rtcm1074 com1 1\r\n")
+ msgs.append("rtcm1084 com1 1\r\n")
+ msgs.append("rtcm1094 com1 1\r\n")
+ msgs.append("rtcm1124 com1 1\r\n")
+ msgs.append("saveconfig\r\n")
+ return msgs
+
+
def config_fixed_ublox(
acc_limit: int, lat: float, lon: float, height: float, posmode: str
) -> UBXMessage:
@@ -381,6 +423,33 @@ def config_fixed_septentrio(
return msgs
+def config_fixed_unicore(
+ acc_limit: int, lat: float, lon: float, height: float, posmode: str
+) -> list:
+ """
+ Configure Fixed mode with specified coordinates for Septentrio receivers.
+
+ :param int acc_limit: accuracy limit in cm
+ :param float lat: lat or X in m
+ :param float lon: lon or Y in m
+ :param float height: height or Z in m
+ :param str posmode: "LLH" or "ECEF"
+ :return: ASCII TTY commands
+ :rtype: list
+ """
+
+ msgs = []
+ msgs.append(f"mode base {lat} {lon} {height}\r\n")
+ msgs.append("rtcm1006 com1 10\r\n")
+ msgs.append("rtcm1033 com1 10\r\n")
+ msgs.append("rtcm1074 com1 1\r\n")
+ msgs.append("rtcm1084 com1 1\r\n")
+ msgs.append("rtcm1094 com1 1\r\n")
+ msgs.append("rtcm1124 com1 1\r\n")
+ msgs.append("saveconfig\r\n")
+ return msgs
+
+
def config_svin_quectel() -> NMEAMessage:
"""
Configure SVIN enable message for Quectel receivers.
diff --git a/src/pygpsclient/recorder_dialog.py b/src/pygpsclient/recorder_dialog.py
index 32f3094a..184eeda9 100644
--- a/src/pygpsclient/recorder_dialog.py
+++ b/src/pygpsclient/recorder_dialog.py
@@ -121,7 +121,7 @@ def _body(self):
Set up frame and widgets.
"""
- self._frm_body = Frame(self.container, borderwidth=2, relief="groove", bg=BGCOL)
+ self._frm_body = Frame(self.container, bg=BGCOL)
self._btn_load = Button(
self._frm_body,
image=self._img_load,
diff --git a/src/pygpsclient/rover_frame.py b/src/pygpsclient/rover_frame.py
index 28f759c0..cc89a924 100644
--- a/src/pygpsclient/rover_frame.py
+++ b/src/pygpsclient/rover_frame.py
@@ -21,6 +21,7 @@
MODE_POL,
TAG_DATA,
TAG_GRID,
+ TAG_WAIT,
TAG_XLABEL,
CanvasCompass,
)
@@ -30,7 +31,7 @@
WIDGETU2,
)
from pygpsclient.helpers import setubxrate
-from pygpsclient.strings import NA
+from pygpsclient.strings import DLGWAITRELPOS, NA
MAXPOINTS = 100
INSET = 4
@@ -69,8 +70,10 @@ def __init__(self, app: Frame, parent: Frame, *args, **kwargs):
self.points = []
self._redraw = True
self._maxdist = 0
+ self._waiting = True
self._body()
self._attach_events()
+ self.enable_messages(True)
def _body(self):
"""Set up frame and widgets."""
@@ -96,7 +99,7 @@ def init_frame(self):
"""
# only redraw the tags that have changed
- tags = (TAG_GRID, TAG_XLABEL) if self._redraw else ()
+ tags = (TAG_GRID, TAG_XLABEL, TAG_WAIT) if self._redraw else ()
maxgrid = 10
scale = self._range / maxgrid / self._scale_c
self._canvas.create_compass(
@@ -117,6 +120,10 @@ def update_frame(self):
center_y = self.height / 2
hdg = self.__app.gnss_status.rel_pos_heading
dis = self.__app.gnss_status.rel_pos_length
+ if hdg not in (0.0, "", None) and dis not in (0.0, "", None):
+ self._waiting = False
+ else:
+ return
acchdg = self.__app.gnss_status.acc_heading
accdis = self.__app.gnss_status.acc_length
try:
@@ -283,6 +290,17 @@ def enable_messages(self, status: int):
setubxrate(self.__app, "NAV-RELPOSNED", status)
+ def _on_clear(self, event): # pylint: disable=unused-argument
+ """ "
+ Clear plot.
+
+ :param Event event: clear event
+ """
+
+ self.points = []
+ self._maxdist = 0
+ self._redraw = True
+
def _on_resize(self, event): # pylint: disable=unused-argument
"""
Resize frame.
@@ -292,17 +310,15 @@ def _on_resize(self, event): # pylint: disable=unused-argument
self.width, self.height = self.get_size()
self._redraw = True
+ self._on_waiting()
- def _on_clear(self, event): # pylint: disable=unused-argument
- """ "
- Clear plot.
-
- :param Event event: clear event
+ def _on_waiting(self):
+ """
+ Display 'waiting for data' alert.
"""
- self.points = []
- self._maxdist = 0
- self._redraw = True
+ if self._waiting:
+ self._canvas.create_alert(DLGWAITRELPOS, tags=TAG_WAIT)
def get_size(self) -> tuple:
"""
diff --git a/src/pygpsclient/scatter_frame.py b/src/pygpsclient/scatter_frame.py
index f5162b5b..a70a1eaa 100644
--- a/src/pygpsclient/scatter_frame.py
+++ b/src/pygpsclient/scatter_frame.py
@@ -50,6 +50,7 @@
MODE_POL,
TAG_DATA,
TAG_GRID,
+ TAG_WAIT,
TAG_XLABEL,
CanvasCompass,
)
@@ -70,6 +71,7 @@
reorder_range,
xy2ll,
)
+from pygpsclient.strings import DLGWAITPOS
AVG = "avg"
CTRAVG = "Average"
@@ -115,6 +117,7 @@ def __init__(self, app: Frame, parent: Frame, *args, **kwargs):
self._stddev = None
self._fixed = None
self._bounds = None
+ self._waiting = True
self._minlat = 100
self._minlon = 200
self._maxlat = -100
@@ -349,7 +352,7 @@ def init_frame(self):
"""
# only redraw the tags that have changed
- tags = (TAG_GRID, TAG_XLABEL) if self._redraw else ()
+ tags = (TAG_GRID, TAG_XLABEL, TAG_WAIT) if self._redraw else ()
scale, unt = self.get_range_label()
if scale >= 100:
dp = 0
@@ -513,6 +516,7 @@ def update_frame(self):
if lat == 0.0 and lon == 0.0: # assume no fix
return
+ self._waiting = False
pos = Point(lat, lon)
if (
@@ -595,6 +599,15 @@ def _on_resize(self, event): # pylint: disable=unused-argument
self.width, self.height = self.get_size()
self._redraw = True
+ self._on_waiting()
+
+ def _on_waiting(self):
+ """
+ Display 'waiting for data' alert.
+ """
+
+ if self._waiting:
+ self._canvas.create_alert(DLGWAITPOS, tags=TAG_WAIT)
def get_size(self) -> tuple:
"""
diff --git a/src/pygpsclient/serverconfig_dialog.py b/src/pygpsclient/serverconfig_dialog.py
index 8cf9b79d..2c3d2c25 100644
--- a/src/pygpsclient/serverconfig_dialog.py
+++ b/src/pygpsclient/serverconfig_dialog.py
@@ -63,6 +63,7 @@
READONLY,
SERVERCONFIG,
TRACEMODE_WRITE,
+ UM980,
VALFLOAT,
VALINT,
VALNONBLANK,
@@ -79,16 +80,19 @@
config_disable_lg290p,
config_disable_septentrio,
config_disable_ublox,
+ config_disable_unicore,
config_fixed_lc29h,
config_fixed_lg290p,
config_fixed_septentrio,
config_fixed_ublox,
+ config_fixed_unicore,
config_nmea,
config_svin_lc29h,
config_svin_lg290p,
config_svin_quectel,
config_svin_septentrio,
config_svin_ublox,
+ config_svin_unicore,
)
from pygpsclient.strings import (
DLGNOTLS,
@@ -130,6 +134,7 @@
BASE_FIXED = "FIXED"
BASE_SVIN = "SURVEY IN"
BASEMODES = (BASE_SVIN, BASE_DISABLED, BASE_FIXED)
+RTKDEVICES = (ZED_F9, UM980, LG290P, LC29H, MOSAIC_X5, ZED_X20)
DURATIONS = (60, 1200, 600, 300, 240, 180, 120, 90)
MAXSVIN = 15
POS_ECEF = "ECEF"
@@ -300,7 +305,7 @@ def _body(self):
)
self._spn_rcvrtype = Spinbox(
self._frm_advanced,
- values=(ZED_F9, ZED_X20, LG290P, LC29H, MOSAIC_X5),
+ values=RTKDEVICES,
width=18,
state=READONLY,
wrap=True,
@@ -1039,6 +1044,8 @@ def _config_disable(self) -> object:
return config_disable_lc29h()
if self.receiver_type.get() == MOSAIC_X5:
return config_disable_septentrio()
+ if self.receiver_type.get() == UM980:
+ return config_disable_unicore()
return config_disable_ublox()
def _config_svin(self, acc_limit: int, svin_min_dur: int) -> object:
@@ -1057,6 +1064,8 @@ def _config_svin(self, acc_limit: int, svin_min_dur: int) -> object:
return config_svin_lc29h(acc_limit, svin_min_dur)
if self.receiver_type.get() == MOSAIC_X5:
return config_svin_septentrio(acc_limit, svin_min_dur)
+ if self.receiver_type.get() == UM980:
+ return config_svin_unicore(acc_limit, svin_min_dur)
return config_svin_ublox(acc_limit, svin_min_dur)
def _config_fixed(
@@ -1080,6 +1089,8 @@ def _config_fixed(
return config_fixed_lc29h(acc_limit, lat, lon, height, posmode)
if self.receiver_type.get() == MOSAIC_X5:
return config_fixed_septentrio(acc_limit, lat, lon, height, posmode)
+ if self.receiver_type.get() == UM980:
+ return config_fixed_unicore(acc_limit, lat, lon, height, posmode)
return config_fixed_ublox(acc_limit, lat, lon, height, posmode)
def _on_quectel_restart(self):
diff --git a/src/pygpsclient/settings_child_frame.py b/src/pygpsclient/settings_child_frame.py
index c8adfb9e..e6e928b8 100644
--- a/src/pygpsclient/settings_child_frame.py
+++ b/src/pygpsclient/settings_child_frame.py
@@ -137,6 +137,7 @@ def __init__(self, app: Frame, parent: Frame, *args, **kwargs):
self._prot_ubx = IntVar()
self._prot_sbf = IntVar()
self._prot_qgc = IntVar()
+ self._prot_unicore = IntVar()
self._prot_rtcm = IntVar()
self._prot_spartn = IntVar()
self._prot_tty = IntVar()
@@ -273,6 +274,11 @@ def _body(self):
text="QGC",
variable=self._prot_qgc,
)
+ self._chk_unicore = Checkbutton(
+ self._frm_options,
+ text="UNI",
+ variable=self._prot_unicore,
+ )
self._chk_tty = Checkbutton(
self._frm_options,
text="TTY",
@@ -454,6 +460,7 @@ def _do_layout(self):
self._chk_qgc.grid(column=2, row=1, padx=0, pady=0, sticky=W)
self._chk_spartn.grid(column=3, row=1, padx=0, pady=0, sticky=W)
self._chk_tty.grid(column=1, row=2, padx=0, pady=0, sticky=W)
+ # self._chk_unicore.grid(column=2, row=2, padx=0, pady=0, sticky=W) # TODO
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
@@ -501,6 +508,7 @@ def _attach_events(self, add: bool = True):
self._prot_ubx.trace_update(tracemode, self._on_update_ubxprot, add)
self._prot_sbf.trace_update(tracemode, self._on_update_sbfprot, add)
self._prot_qgc.trace_update(tracemode, self._on_update_qgcprot, add)
+ self._prot_unicore.trace_update(tracemode, self._on_update_uniprot, add)
self._prot_nmea.trace_update(tracemode, self._on_update_nmeaprot, add)
self._prot_rtcm.trace_update(tracemode, self._on_update_rtcmprot, add)
self._prot_spartn.trace_update(tracemode, self._on_update_spartnprot, add)
@@ -578,6 +586,14 @@ def _on_update_qgcprot(self, var, index, mode):
if not self._prot_tty.get():
self.__app.configuration.set("qgcprot_b", self._prot_qgc.get())
+ def _on_update_uniprot(self, var, index, mode):
+ """
+ Action on updating unicoreprot.
+ """
+
+ if not self._prot_tty.get():
+ self.__app.configuration.set("uniprot_b", self._prot_unicore.get())
+
def _on_update_nmeaprot(self, var, index, mode):
"""
Action on updating nmeaprot.
@@ -618,6 +634,7 @@ def _on_update_ttyprot(self, var, index, mode):
self._prot_ubx,
self._prot_sbf,
self._prot_qgc,
+ self._prot_unicore,
self._prot_rtcm,
self._prot_spartn,
):
@@ -627,6 +644,7 @@ def _on_update_ttyprot(self, var, index, mode):
self._prot_ubx.set(cfg.get("ubxprot_b"))
self._prot_sbf.set(cfg.get("sbfprot_b"))
self._prot_qgc.set(cfg.get("qgcprot_b"))
+ self._prot_unicore.set(cfg.get("uniprot_b"))
self._prot_rtcm.set(cfg.get("rtcmprot_b"))
self._prot_spartn.set(cfg.get("spartnprot_b"))
cfg.set("ttyprot_b", tty)
@@ -827,8 +845,8 @@ def _on_connect(self, conntype: int):
self.__app.conn_status = conntype
self.__app.conn_label = (connstr, OKCOL)
self.__app.status_label = ("", INFOCOL)
- self.__app.reset_frames()
- self.__app.reset_gnssstatus()
+ 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)
def enable_controls(self, status: int):
diff --git a/src/pygpsclient/settings_frame.py b/src/pygpsclient/settings_frame.py
index 6e99845c..fe12d1f2 100644
--- a/src/pygpsclient/settings_frame.py
+++ b/src/pygpsclient/settings_frame.py
@@ -23,11 +23,12 @@ class SettingsFrame(Frame):
Settings frame class.
"""
- def __init__(self, app: Frame, *args, **kwargs):
+ def __init__(self, app: Frame, parent: Frame, *args, **kwargs):
"""
Constructor.
:param Frame 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
"""
@@ -35,7 +36,7 @@ def __init__(self, app: Frame, *args, **kwargs):
self.__app = app # Reference to main application class
self.__master = self.__app.appmaster # Reference to root class (Tk)
- super().__init__(self.__master, *args, **kwargs)
+ super().__init__(parent, *args, **kwargs)
self._container() # create scrollable container
self._body()
diff --git a/src/pygpsclient/signalsview_frame.py b/src/pygpsclient/signalsview_frame.py
index 1aeaa50a..42775d11 100644
--- a/src/pygpsclient/signalsview_frame.py
+++ b/src/pygpsclient/signalsview_frame.py
@@ -22,6 +22,7 @@
from pygpsclient.canvas_subclasses import (
TAG_DATA,
TAG_GRID,
+ TAG_WAIT,
TAG_XLABEL,
TAG_YLABEL,
CanvasGraph,
@@ -32,17 +33,16 @@
GNSS_LIST,
GRIDMAJCOL,
MAX_SNR,
+ MAXWAIT,
PNTCOL,
SIGNALSVIEW,
WIDGETU3,
)
from pygpsclient.helpers import col2contrast, fitfont, setubxrate
-from pygpsclient.strings import DLGENABLENAVSIG, DLGNONAVSIG, DLGWAITNAVSIG
+from pygpsclient.strings import DLGNONAVSIG, DLGWAITNAVSIG
OL_WID = 1
FONTSCALELG = 40
-MAXWAIT = 10
-ACTIVE = ""
XLBLANGLE = 60
XLBLFMT = "000 WWW_W/W"
# Correction source legend
@@ -88,11 +88,12 @@ def __init__(self, app: Frame, parent: Frame, *args, **kwargs):
self.width = kwargs.get("width", def_w)
self.height = kwargs.get("height", def_h)
self._redraw = True
- self._navsig_status = DLGENABLENAVSIG
self._pending_confs = {}
self._waits = 0
+ self._waiting = True
self._body()
self._attach_events()
+ self.enable_messages(True)
def _body(self):
"""
@@ -150,7 +151,6 @@ def enable_messages(self, status: bool):
setubxrate(self.__app, "NAV-SIG", status)
for msgid in ("ACK-ACK", "ACK-NAK"):
self._set_pending(msgid, SIGNALSVIEW)
- self._navsig_status = DLGWAITNAVSIG
def _set_pending(self, msgid: int, ubxfrm: int):
"""
@@ -185,7 +185,6 @@ def update_pending(self, msg: UBXMessage):
tags=TAG_DATA,
)
self._pending_confs.pop("ACK-NAK")
- self._navsig_status = DLGNONAVSIG
if self._pending_confs.get("ACK-ACK", False):
self._pending_confs.pop("ACK-ACK")
@@ -205,7 +204,7 @@ def init_frame(self):
"""
# only redraw the tags that have changed
- tags = (TAG_GRID, TAG_XLABEL, TAG_YLABEL) if self._redraw else ()
+ tags = (TAG_GRID, TAG_XLABEL, TAG_YLABEL, TAG_WAIT) if self._redraw else ()
self._canvas.create_graph(
xdatamax=10,
ydatamax=(MAX_SNR,),
@@ -221,15 +220,6 @@ def init_frame(self):
)
self._redraw = False
- # display 'enable NAV-SIG' warning
- self._canvas.create_text(
- self.width / 2,
- self.height / 2,
- text=self._navsig_status,
- fill=PNTCOL,
- tags=TAG_DATA,
- )
-
def _draw_legend(self):
"""
Draw GNSS color code and correction source legends
@@ -262,7 +252,9 @@ def _draw_legend(self):
)
# correction source legend
- xfnt, _, _ = fitfont(CL, self.width / 2 - self._canvas.xoffl, h / 2, maxsiz=12)
+ xfnt, _, _, _ = fitfont(
+ CL, self.width / 2 - self._canvas.xoffl, h / 2, maxsiz=12
+ )
self._canvas.create_text(
self.width / 2,
self._canvas.yofft + 1,
@@ -282,12 +274,12 @@ def update_frame(self):
data = self.__app.gnss_status.sig_data
if len(data) == 0:
if self._waits >= MAXWAIT:
- self._navsig_status = DLGNONAVSIG
+ self._canvas.create_alert(DLGNONAVSIG, tags=TAG_WAIT)
else:
self._waits += 1
else:
+ self._waiting = False
self._waits = 0
- self._navsig_status = ACTIVE
show_unused = self.__app.configuration.get("unusedsat_b")
siv = len(data)
siv = siv if show_unused else siv - unused_sigs(data)
@@ -299,7 +291,7 @@ def update_frame(self):
offset = self._canvas.xoffl
colwidth = (w - self._canvas.xoffl - self._canvas.xoffr + 1) / siv
- xfnt, _, _ = fitfont(
+ xfnt, _, _, _ = fitfont(
XLBLFMT,
colwidth * 1.66,
self._canvas.yoffb,
@@ -360,6 +352,16 @@ def _on_resize(self, event): # pylint: disable=unused-argument
self.width, self.height = self.get_size()
self._redraw = True
+ self._on_waiting()
+
+ def _on_waiting(self):
+ """
+ Display 'waiting for data' alert.
+ """
+
+ if self._waiting:
+ txt = DLGNONAVSIG if self._waits >= MAXWAIT else DLGWAITNAVSIG
+ self._canvas.create_alert(txt, tags=TAG_WAIT)
def get_size(self):
"""
diff --git a/src/pygpsclient/skyview_frame.py b/src/pygpsclient/skyview_frame.py
index 6bf1579c..7f1e40b8 100644
--- a/src/pygpsclient/skyview_frame.py
+++ b/src/pygpsclient/skyview_frame.py
@@ -20,6 +20,7 @@
MODE_CEL,
TAG_DATA,
TAG_GRID,
+ TAG_WAIT,
TAG_XLABEL,
CanvasCompass,
)
@@ -30,6 +31,7 @@
WIDGETU2,
)
from pygpsclient.helpers import col2contrast, snr2col, unused_sats
+from pygpsclient.strings import DLGWAITAZI
OL_WID = 4
FONTSCALE = 30
@@ -61,6 +63,7 @@ def __init__(self, app: Frame, parent: Frame, *args, **kwargs):
self.bg_col = BGCOL
self.fg_col = FGCOL
self._redraw = True
+ self._waiting = True
self._body()
self._attach_events()
@@ -94,7 +97,7 @@ def init_frame(self):
"""
# only redraw the tags that have changed
- tags = (TAG_GRID, TAG_XLABEL) if self._redraw else ()
+ tags = (TAG_GRID, TAG_XLABEL, TAG_WAIT) if self._redraw else ()
self._canvas.create_compass(
fontscale=FONTSCALE,
tags=tags,
@@ -110,15 +113,23 @@ def update_frame(self):
show_unused = self.__app.configuration.get("unusedsat_b")
siv = len(data)
siv = siv if show_unused else siv - unused_sats(data)
- if siv <= 0:
+ sel = sum(1 for (_, _, ele, _, _, _) in data.values() if ele not in ("", None))
+ # ignore if cno are all zero and 'show_unused' is not set,
+ # or if elevation values are all null
+ if siv <= 0 or sel <= 0:
return
+ self._waiting = False
self.init_frame()
for val in sorted(data.values(), key=lambda x: x[4]): # sort by ascending C/N0
try:
gnssId, prn, ele, azi, cno, _ = val
- if cno == 0 and not show_unused:
+ if (
+ (cno == 0 and not show_unused)
+ or ele in ("", None)
+ or azi in ("", None)
+ ):
continue
x, y = self._canvas.d2xy(int(azi), int(ele))
_, ol_col = GNSS_LIST[gnssId]
@@ -155,6 +166,15 @@ def _on_resize(self, event): # pylint: disable=unused-argument
self.width, self.height = self.get_size()
self._redraw = True
+ self._on_waiting()
+
+ def _on_waiting(self):
+ """
+ Display 'waiting for data' alert.
+ """
+
+ if self._waiting:
+ self._canvas.create_alert(DLGWAITAZI, tags=TAG_WAIT)
def get_size(self):
"""
diff --git a/src/pygpsclient/spectrum_frame.py b/src/pygpsclient/spectrum_frame.py
index 0849a06f..b891d12d 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, EW, NSEW, NW, Checkbutton, Frame, IntVar, N, S, W
+from tkinter import ALL, CENTER, EW, NSEW, NW, Checkbutton, Frame, IntVar, N, S, W
from types import NoneType
from pyubx2 import UBXMessage
@@ -24,6 +24,7 @@
from pygpsclient.canvas_subclasses import (
TAG_DATA,
TAG_GRID,
+ TAG_WAIT,
TAG_XLABEL,
TAG_YLABEL,
CanvasGraph,
@@ -32,13 +33,14 @@
BGCOL,
FGCOL,
GNSS_LIST,
+ MAXWAIT,
PLOTCOLS,
PNTCOL,
SPECTRUMVIEW,
WIDGETU2,
)
from pygpsclient.helpers import setubxrate
-from pygpsclient.strings import DLGENABLEMONSPAN, DLGNOMONSPAN, DLGWAITMONSPAN
+from pygpsclient.strings import DLGNOMONSPAN, DLGWAITMONSPAN
# Graph dimensions
OL_WID = 1
@@ -85,7 +87,6 @@
MODEINIT = "init"
MODELIVE = "live"
MODESNAP = "snap"
-MAXWAIT = 10
GHZ = 1e9
FONTSCALE = 35
@@ -118,7 +119,6 @@ def __init__(self, app: Frame, parent: Frame, *args, **kwargs):
self._maxdb = MAX_DB
self._minhz = MIN_HZ
self._maxhz = MAX_HZ
- self._monspan_status = DLGENABLEMONSPAN
self._pending_confs = {}
self._showrf = True
self._chartpos = None
@@ -126,8 +126,10 @@ def __init__(self, app: Frame, parent: Frame, *args, **kwargs):
self._pgaoffset = IntVar()
self._waits = 0
self._redraw = True
+ self._waiting = True
self._body()
self._attach_events()
+ self.enable_messages(True)
def _body(self):
"""
@@ -160,7 +162,9 @@ def _attach_events(self):
self._canvas.bind("", self._on_click)
self._canvas.bind("", self._on_toggle_rf)
self._canvas.bind("", self._on_snapshot)
+ self._canvas.bind("", self._on_snapshot)
self._canvas.bind("", self._on_clear_snapshot)
+ self._canvas.bind("", self._on_clear_snapshot)
self._pgaoffset.trace_add(("write", "unset"), self._on_update_pga)
def reset(self):
@@ -195,7 +199,6 @@ def enable_messages(self, status: bool):
setubxrate(self.__app, "MON-SPAN", status)
for msgid in ("ACK-ACK", "ACK-NAK"):
self._set_pending(msgid, SPECTRUMVIEW)
- self._monspan_status = DLGWAITMONSPAN
def _set_pending(self, msgid: int, ubxfrm: int):
"""
@@ -229,7 +232,6 @@ def update_pending(self, msg: UBXMessage):
anchor=S,
)
self._pending_confs.pop("ACK-NAK")
- self._monspan_status = DLGNOMONSPAN
if self._pending_confs.get("ACK-ACK", False):
self._pending_confs.pop("ACK-ACK")
@@ -245,12 +247,12 @@ def update_frame(self):
rfblocks = self.__app.gnss_status.spectrum_data
if len(rfblocks) == 0:
if self._waits >= MAXWAIT:
- self._monspan_status = DLGNOMONSPAN
+ self._canvas.create_alert(DLGNOMONSPAN, tags=TAG_WAIT)
else:
self._waits += 1
- else:
- self._waits = 0
- self._monspan_status = ACTIVE
+ return
+ self._waits = 0
+ self._waiting = False
self._update_plot(rfblocks)
if self._spectrum_snapshot != []:
@@ -262,7 +264,7 @@ def init_frame(self):
"""
# only redraw the tags that have changed
- tags = (TAG_GRID, TAG_XLABEL, TAG_YLABEL) if self._redraw else ()
+ tags = (TAG_GRID, TAG_XLABEL, TAG_YLABEL, TAG_WAIT) if self._redraw else ()
# draw graph axes and labels
self._canvas.create_graph(
xdatamax=self._maxhz / GHZ,
@@ -284,15 +286,6 @@ def init_frame(self):
)
self._redraw = False
- # display 'enable MON-SPAN' warning
- self._canvas.create_text(
- self.width / 2,
- self.height / 2,
- text=self._monspan_status,
- fill="orange",
- tags=TAG_DATA,
- )
-
def _update_plot(
self, rfblocks: list, mode: str = MODELIVE, colors: dict | NoneType = None
):
@@ -445,7 +438,7 @@ def _plot_marker(self, mode):
text=f"{hz:.3f} GHz\n{db:.1f} dB",
fill=FGCOL,
font=self._canvas.font,
- anchor="center",
+ anchor=CENTER,
tags=(TAG_XLABEL, mode),
)
@@ -527,6 +520,16 @@ def _on_resize(self, event): # pylint: disable=unused-argument
self.width, self.height = self.get_size()
self._chartpos = None
self._redraw = True
+ self._on_waiting()
+
+ def _on_waiting(self):
+ """
+ Display 'waiting for data' alert.
+ """
+
+ if self._waiting:
+ txt = DLGNOMONSPAN if self._waits >= MAXWAIT else DLGWAITMONSPAN
+ self._canvas.create_alert(txt, tags=TAG_WAIT)
def get_size(self):
"""
diff --git a/src/pygpsclient/sqlite_handler.py b/src/pygpsclient/sqlite_handler.py
index a9bea021..272d9034 100644
--- a/src/pygpsclient/sqlite_handler.py
+++ b/src/pygpsclient/sqlite_handler.py
@@ -26,13 +26,14 @@
import traceback
from datetime import datetime, timezone
from os import path
+from pathlib import Path
from types import NoneType
from pynmeagps import ecef2llh
from pygpsclient.globals import ERRCOL, HOME, INFOCOL, OKCOL
from pygpsclient.helpers import makeval
-from pygpsclient.strings import NA
+from pygpsclient.strings import DLGDBINIT, DLGDBINITERR, DLGDBOPEN, DLGDBSQLERR, NA
# path to mod_spatialite module, if required
# SLPATH = "C:/Program Files/QGIS 3.44.1/bin"
@@ -73,6 +74,9 @@
)
"""SQL for creating database and table with lat/lon/hmsl as 3D POINTZ"""
+SQLINIT = "SELECT InitSpatialMetaData();"
+"""SQL for initialising spatial metadata"""
+
SQLI3D = (
"INSERT INTO {table} (geom, utc, fix, hae, speed, track, siv, sip, pdop, "
"hdop, vdop, hacc, vacc, diffcorr, diffage, diffstat, baselon, baselat, basehae) "
@@ -128,34 +132,28 @@ def _create(
"""
try:
- self.__app.status_label = (
- f"Database {self._db} initialising - please wait...",
- INFOCOL,
- )
+ self.__app.status_label = (DLGDBINIT.format(self._db), INFOCOL)
self.logger.debug("Spatial metadata initialisation in progress...")
- self._connection.execute("SELECT InitSpatialMetaData();")
+ self._connection.execute(SQLINIT)
self.logger.debug("Spatial metadata initialisation complete")
self._cursor = self._connection.cursor()
self._cursor.executescript(SQLC1.format(table=tbname))
return SQLOK
except sqlite3.Error as err:
- self.__app.status_label = (
- f"Error initialising spatial database {err}",
- ERRCOL,
- )
+ self.__app.status_label = (DLGDBINITERR.format(err), ERRCOL)
self.logger.debug(traceback.format_exc())
return SQLERR
def open(
self,
- dbpath: str = HOME,
+ dbpath: str | Path = HOME,
dbname: str = DBNAME,
tbname: str = TBNAME,
) -> str | int:
"""
Create sqlite3 connection and cursor.
- :param str dbpath: path to sqlite3 database file
+ :param str | Path dbpath: path to sqlite3 database file
:param str dbname: name of sqlite3 database file
:param str tbname: name of table containing gnss data
:return: result
@@ -186,18 +184,18 @@ def open(
if testing:
self._connection.close()
else:
- self.__app.status_label = (f"Database {self._db} opened", OKCOL)
+ self.__app.status_label = (DLGDBOPEN.format(self._db), OKCOL)
return SQLOK
except AttributeError as err:
- self.__app.status_label = (f"SQL error: {err}", errcol)
+ self.__app.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 = (f"SQL error {db}: {err}", errcol)
+ self.__app.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 = (f"SQL error {db}: {err}", errcol)
+ self.__app.status_label = (DLGDBSQLERR.format(err), errcol)
self.logger.debug(traceback.format_exc())
return SQLERR # other sqlite error
@@ -213,13 +211,13 @@ def close(self):
return
self._connection.close()
- def load_data(self, ignore_null: bool = True) -> str:
+ def load_data(self, ignore_null: bool = True) -> int:
"""
Load current gnss data (from `self.__app.gnss_status`) into database.
:param bool ignore_null: ignore null position flag
:return: result
- :rtype: str
+ :rtype: int
"""
gnss = self.__app.gnss_status
@@ -262,7 +260,7 @@ def load_data(self, ignore_null: bool = True) -> str:
self.logger.debug(f"Executed SQL statement {sql}")
return SQLOK
except sqlite3.Error as err:
- self.__app.status_label = (f"SQL error: {err}", ERRCOL)
+ self.__app.status_label = (DLGDBSQLERR.format(err), ERRCOL)
self.logger.debug(traceback.format_exc())
return SQLERR
@@ -279,7 +277,7 @@ def database(self) -> str | NoneType:
def retrieve_data(
- dbpath: str = path.join(HOME, DBNAME),
+ dbpath: str | Path = path.join(HOME, DBNAME),
table: str = TBNAME,
sqlwhere: str = "",
limit: int = 100,
@@ -288,7 +286,7 @@ def retrieve_data(
"""
Retrieve specified rows from sqlite table, ordered by utc timestamp.
- :param str dbpath: fully qualified path to database
+ :param str | Path dbpath: fully qualified path to database
:param str table: name of database table
:param str sqlwhere: optional SQL WHERE clause
:param int limit: SQL LIMIT number of rows
@@ -299,6 +297,7 @@ def retrieve_data(
:raises: sqlite3.Error
"""
+ sql = ""
try:
if not path.exists(dbpath):
raise FileNotFoundError(f"No such database: '{dbpath}'")
diff --git a/src/pygpsclient/status_frame.py b/src/pygpsclient/status_frame.py
index a71d6cc2..253867fb 100644
--- a/src/pygpsclient/status_frame.py
+++ b/src/pygpsclient/status_frame.py
@@ -1,5 +1,5 @@
"""
-status_frane.py
+status_frame.py
Status Bar frame class for PyGPSClient application.
@@ -14,17 +14,20 @@
from tkinter import EW, NS, VERTICAL, Frame, Label, W, ttk
+from pygpsclient.globals import BGCOL
+
class StatusFrame(Frame):
"""
Status bar frame class.
"""
- def __init__(self, app, *args, **kwargs):
+ def __init__(self, app: Frame, parent: Frame, *args, **kwargs):
"""
Constructor
:param Frame 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
"""
@@ -32,7 +35,7 @@ def __init__(self, app, *args, **kwargs):
self.__app = app # Reference to main application class
self.__master = self.__app.appmaster # Reference to root class (Tk)
- super().__init__(self.__master, *args, **kwargs)
+ super().__init__(parent, *args, **kwargs)
self.width, self.height = self.get_size()
self._body()
@@ -44,8 +47,8 @@ def _body(self):
Set up frame and widgets.
"""
- self.lbl_connection = Label(self, anchor=W)
- self.lbl_status = Label(self, anchor=W)
+ self.lbl_connection = Label(self, anchor=W, bg=BGCOL)
+ self.lbl_status = Label(self, anchor=W, bg=BGCOL)
def _do_layout(self):
"""
diff --git a/src/pygpsclient/stream_handler.py b/src/pygpsclient/stream_handler.py
index 385536ed..e26e3f65 100644
--- a/src/pygpsclient/stream_handler.py
+++ b/src/pygpsclient/stream_handler.py
@@ -26,6 +26,8 @@ class to read and parse incoming data from the receiver. It places
:license: BSD 3-Clause
"""
+# pylint: disable=fixme
+
import logging
import ssl
from datetime import datetime, timedelta
@@ -45,7 +47,7 @@ class to read and parse incoming data from the receiver. It places
from tkinter import Frame, Label, Tk
from certifi import where as findcacerts
-from pygnssutils import (
+from pygnssutils import ( # UNI_PROTOCOL # TODO
NMEA_PROTOCOL,
QGC_PROTOCOL,
RTCM3_PROTOCOL,
@@ -321,6 +323,7 @@ def _errorhandler(err: Exception):
| SBF_PROTOCOL
| QGC_PROTOCOL
| RTCM3_PROTOCOL,
+ # | UNI_PROTOCOL, # TODO
quitonerror=ERR_LOG,
bufsize=DEFAULT_BUFSIZE,
msgmode=settings["msgmode"],
@@ -383,6 +386,9 @@ def _errorhandler(err: Exception):
QGCMessageError,
QGCParseError,
QGCStreamError,
+ # UNIMessageError,
+ # UNIParseError,
+ # UNIStreamError,
GNSSError,
) as err:
_errorhandler(err)
diff --git a/src/pygpsclient/strings.py b/src/pygpsclient/strings.py
index 47aad1c8..f08ef8b1 100644
--- a/src/pygpsclient/strings.py
+++ b/src/pygpsclient/strings.py
@@ -36,11 +36,12 @@
BREWWARN = "Function unavailable under Homebrew"
BREWUPDATE = "In-app update not available under Homebrew. Use terminal."
CONFIGBAD = "{} command rejected"
-CONFIGERR = "Invalid configuration data"
+CONFIGERR = "Invalid configuration data {}"
CONFIGOK = "{} command accepted"
CONFIGRXM = "{} polled, {} key(s) loaded"
CONFIGTITLE = "Config File"
CONFIRM = "CONFIRM"
+DGPSYES = "\u2713" # tick symbol
ENDOFFILE = "End of file reached"
FILEOPENERROR = "Error opening file {}"
HALTTAGWARN = "HALTED ON USER TAG MATCH: {}"
@@ -53,11 +54,11 @@
LOADCONFIGRESAVE = ". Consider re-saving"
MAPCONFIGERR = "Custom map configuration error"
MAPOPENERR = "Unable to open custom map:\n{}"
+MAPPERMERR = "Permission denied to open custom map:\n{}"
MQTTCONN = "Connecting to MQTT server {}..."
NMEAVALERROR = "Value error in NMEA message: {}"
NOCONN = "NO CONNECTION"
NOTCONN = "Not connected"
-NOWDGSWARN = "WARNING! No widgets are enabled in config file {} - display will be blank"
NOWEBMAP = "Unable to display map."
NOWEBMAPCONN = NOWEBMAP + "\nCheck internet connection."
NOWEBMAPFIX = NOWEBMAP + "\nNo satellite fix."
@@ -167,39 +168,45 @@
# Dialog text
DLG = "dlg"
-DLGENABLEMONSPAN = "Enable or poll MON-SPAN message"
-DLGENABLEMONSYS = "Enable or poll MON-SYS/COMMS messages"
-DLGENABLENAVSIG = "Enable or poll NAV-SIG message"
+DLGACTION = "Confirm Command"
+DLGACTIONCONFIRM = "Are you sure?"
+DLGDBINIT = "Database {} initialising - this could take up to a minute..."
+DLGDBINITERR = "Error initialising spatial database {}"
+DLGDBOPEN = "Database {} opened"
+DLGDBSQLERR = "SQL error {}"
DLGGPXERROR = "GPX Parsing Error!"
-DLGGPXOPEN = "Click folder icon to open GPX Track file"
DLGGPXLOAD = "Loading GPX file ..."
DLGGPXLOADED = "GPX data processed"
+DLGGPXNOMINAL = "time and speed axes are nominal"
DLGGPXNULL = "No <{}> elements in GPX File!"
DLGGPXOOB = "Map out of bounds\nTry increasing zoom level"
+DLGGPXOPEN = "Click folder icon to open GPX Track file"
DLGGPXWAIT = "Redrawing..."
-DLGGPXNOMINAL = "time and speed axes are nominal"
DLGHOWTO = f"How To Use {TITLE}"
DLGJSONERR = "Error! {}"
DLGJSONOK = "Keys loaded from {}"
-DLGNOMONSPAN = "This receiver does not appear to\nsupport MON-SPAN messages"
-DLGNOMONSYS = "This receiver does not appear to\nsupport MON-SYS/COMMS messages"
-DLGNONAVSIG = "This receiver does not appear to\nsupport NAV-SIG messages"
-DLGACTION = "Confirm Command"
-DLGACTIONCONFIRM = "Are you sure?"
+DLGNOMONSPAN = "This receiver does not appear to\nsupport UBX MON-SPAN msgs"
+DLGNOMONSYS = "This receiver does not appear to\nsupport UBX MON-SYS/COMMS msgs"
+DLGNONAVSIG = "This receiver does not appear to\nsupport UBX NAV-SIG msgs"
+DLGNOTLS = "TLS PEM file '{hostpem}' not found"
DLGSPARTNWARN = "WARNING! Disconnect from {} client before using {} client"
-DLGWAITMONSPAN = "Waiting for MON-SPAN message..."
-DLGWAITMONSYS = "Waiting for MON-SYS/COMMS messages..."
-DLGWAITNAVSIG = "Waiting for NAV-SIG message..."
DLGSTOPRTK = "WARNING! Stop all active connections before loading configuration"
DLGTABOUT = f"About {TITLE}"
DLGTGPX = "GPX Track Viewer"
+DLGTIMPORTMAP = "Import Custom Map"
+DLGTNMEA = "NMEA Configuration"
DLGTNTRIP = "NTRIP Configuration"
+DLGTRECORD = "Configuration Command Recorder"
DLGTSERVER = "Server Configuration"
DLGTSETTINGS = "Settings"
-DLGTRECORD = "Configuration Command Recorder"
DLGTSPARTN = "SPARTN Configuration"
-DLGTNMEA = "NMEA Configuration"
-DLGTUBX = "UBX Configuration"
-DLGTIMPORTMAP = "Import Custom Map"
DLGTTTY = "TTY Commands"
-DLGNOTLS = "TLS PEM file '{hostpem}' not found"
+DLGTUBX = "UBX Configuration"
+DLGWAITAZI = "Waiting for ELV/AZI data"
+DLGWAITCNO = "Waiting for CNO data"
+DLGWAITIMU = "Waiting for IMU data"
+DLGWAITMONSPAN = "Waiting for UBX MON-SPAN data"
+DLGWAITMONSYS = "Waiting for UBX MON-SYS data"
+DLGWAITNAVSIG = "Waiting for UBX NAV-SIG data"
+DLGWAITPOS = "Waiting for LAT/LON data"
+DLGWAITRELPOS = "Waiting for RELPOS data"
diff --git a/src/pygpsclient/sysmon_frame.py b/src/pygpsclient/sysmon_frame.py
index c351de75..209901db 100644
--- a/src/pygpsclient/sysmon_frame.py
+++ b/src/pygpsclient/sysmon_frame.py
@@ -14,25 +14,27 @@
:license: BSD 3-Clause
"""
-from tkinter import ALL, EW, NSEW, NW, Canvas, E, Frame, IntVar, Radiobutton, W
+from tkinter import EW, NSEW, NW, Canvas, E, Frame, IntVar, Radiobutton, W
from pyubx2 import BOOTTYPE, UBXMessage
-from pygpsclient.globals import BGCOL, FGCOL, PNTCOL, SYSMONVIEW, WIDGETU2
+from pygpsclient.canvas_subclasses import TAG_DATA, TAG_WAIT
+from pygpsclient.globals import BGCOL, FGCOL, MAXWAIT, PNTCOL, SYSMONVIEW, WIDGETU2
from pygpsclient.helpers import (
bytes2unit,
+ fitfont,
hsv2rgb,
- scale_font,
secs2unit,
setubxrate,
)
-from pygpsclient.strings import DLGENABLEMONSYS, DLGNOMONSYS, DLGWAITMONSYS, NA
+from pygpsclient.strings import DLGNOMONSYS, DLGWAITMONSYS, NA
-MINFONT = 6 # minimum font size
-MAXTEMP = 100 # °C
-INSET = 4
-SPACING = 5
+ACTIVE = ""
DASH = (5, 2)
+FONTSCALE = 25
+INSET = 4
+MAXLINES = 19
+MAXTEMP = 100 # °C
PORTIDS = {
0x0000: "I2C", # 0 I2C
0x0100: "UART1", # 256 UART1
@@ -42,10 +44,7 @@
0x0300: "USB", # 768 USB
0x0400: "SPI", # 1024 SPI
}
-ACTIVE = ""
-MAXLINES = 23
-MINFONT = 4
-MAXWAIT = 10
+SPACING = 2
class SysmonFrame(Frame):
@@ -71,23 +70,24 @@ def __init__(self, app: Frame, parent: Frame, *args, **kwargs):
def_w, def_h = WIDGETU2
self.width = kwargs.get("width", def_w)
self.height = kwargs.get("height", def_h)
- self._monsys_status = DLGENABLEMONSYS
self._pending_confs = {}
self._maxtemp = 0
self._waits = 0
+ self._waiting = True
self._mode = IntVar()
self._mode.set(0)
self._font = self.__app.font_sm
self._fonth = self._font.metrics("linespace")
self._body()
self._attach_events()
+ self.enable_messages(True)
def _body(self):
"""
Set up frame and widgets.
"""
- self._can_sysmon = Canvas(self, width=self.width, height=self.height, bg=BGCOL)
+ self._canvas = Canvas(self, width=self.width, height=self.height, bg=BGCOL)
self._frm_status = Frame(self, bg=BGCOL)
self._rad_actual = Radiobutton(
self._frm_status,
@@ -96,7 +96,6 @@ def _body(self):
value=0,
fg=PNTCOL,
bg=BGCOL,
- # selectcolor=BGCOL,
)
self._rad_pending = Radiobutton(
self._frm_status,
@@ -105,9 +104,8 @@ def _body(self):
value=1,
fg=PNTCOL,
bg=BGCOL,
- # selectcolor=BGCOL,
)
- self._can_sysmon.grid(column=0, row=0, padx=0, pady=0, sticky=NSEW)
+ self._canvas.grid(column=0, row=0, padx=0, pady=0, sticky=NSEW)
self._frm_status.grid(column=0, row=1, padx=2, pady=2, sticky=EW)
self._rad_actual.grid(column=0, row=0, padx=0, pady=0, sticky=W)
self._rad_pending.grid(column=1, row=0, padx=0, pady=0, sticky=W)
@@ -120,20 +118,14 @@ def _attach_events(self):
"""
self.bind("", self._on_resize)
- self._can_sysmon.bind("", self._on_clear)
+ self._canvas.bind("", self._on_clear)
def init_chart(self):
"""
Initialise sysmon chart.
"""
- self._can_sysmon.delete(ALL)
- self._can_sysmon.create_text(
- self.width / 2,
- self.height / 2,
- text=self._monsys_status,
- fill="orange",
- )
+ self._canvas.delete(TAG_DATA)
def _on_clear(self, event): # pylint: disable=unused-argument
"""
@@ -145,7 +137,6 @@ def _on_clear(self, event): # pylint: disable=unused-argument
self.__app.gnss_status.sysmon_data = {}
self.__app.gnss_status.comms_data = {}
self._maxtemp = 0
- self._monsys_status = DLGWAITMONSYS
self.init_chart()
def enable_messages(self, status: int):
@@ -162,7 +153,6 @@ def enable_messages(self, status: int):
setubxrate(self.__app, msgid, status)
for msgid in ("ACK-ACK", "ACK-NAK"):
self._set_pending(msgid, SYSMONVIEW)
- self._monsys_status = DLGWAITMONSYS
self.init_chart()
def _set_pending(self, msgid: int, ubxfrm: int):
@@ -187,8 +177,6 @@ def update_pending(self, msg: UBXMessage):
pending = self._pending_confs.get(msg.identity, False)
if pending and msg.identity == "ACK-NAK":
self._pending_confs.pop("ACK-NAK")
- self._monsys_status = DLGNOMONSYS
-
if self._pending_confs.get("ACK-ACK", False):
self._pending_confs.pop("ACK-ACK")
@@ -209,11 +197,12 @@ def update_frame(self):
if len(sysdata) + len(commsdata) == 0:
self._waits += 1
if self._waits >= MAXWAIT:
- self._monsys_status = DLGNOMONSYS
+ self._canvas.create_alert(DLGNOMONSYS, tags=TAG_WAIT)
self.init_chart()
return
- self._monsys_status = ACTIVE
+ self._waiting = False
+ self._canvas.delete(TAG_WAIT)
self._waits = 0
try:
bootType = BOOTTYPE[sysdata.get("bootType", 0)]
@@ -251,16 +240,16 @@ def update_frame(self):
+ f"Runtime: {rtm:{rtf}} {rtmu}\n"
+ f"Notices: {noticeCount}, Warnings: {warnCount}, Errors: {errorCount}"
)
- self._can_sysmon.create_text(
+ self._canvas.create_text(
INSET,
y,
text=txt,
fill=FGCOL,
anchor=NW,
font=self._font,
+ tags=TAG_DATA,
)
except KeyError: # invalid sysmon-data or comms-data
- self._monsys_status = DLGNOMONSYS
self.init_chart()
def _chart_parm(
@@ -278,17 +267,18 @@ def _chart_parm(
scale = (self.width - (3 * xoffset)) / 100
x = xoffset
- self._can_sysmon.create_text(
+ self._canvas.create_text(
x,
y,
text=f"{lbl}: {val} {unit}",
fill=FGCOL,
anchor=W,
font=self._font,
+ tags=TAG_DATA,
)
y += self._fonth
if isinstance(maxval, (int, float)):
- self._can_sysmon.create_line(
+ self._canvas.create_line(
x,
y,
x + maxval * scale,
@@ -296,15 +286,17 @@ def _chart_parm(
fill=self._set_col(maxval),
dash=DASH,
width=self._fonth,
+ tags=TAG_DATA,
)
if isinstance(val, (int, float)):
- self._can_sysmon.create_line(
+ self._canvas.create_line(
x,
y,
x + val * scale,
y,
fill=self._set_col(val),
width=self._fonth,
+ tags=TAG_DATA,
)
y += self._fonth + SPACING
return y
@@ -332,25 +324,27 @@ def _chart_io(self, xoffset: int, y: int, port: int, pdata: tuple):
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}:"
- self._can_sysmon.create_text( # port
+ self._canvas.create_text( # port
x,
y,
text=txt,
fill=FGCOL,
anchor=W,
font=self._font,
+ tags=TAG_DATA,
)
- self._can_sysmon.create_text( # port
+ self._canvas.create_text( # port
x + cap - 1,
y,
text="⇄",
fill=FGCOL,
anchor=E,
font=self._font,
+ tags=TAG_DATA,
)
p = -1
for i in range(0, 8, 4): # RX & TX
- self._can_sysmon.create_line( # max
+ self._canvas.create_line( # max
x + cap,
y + p,
x + cap + pdata[i + 1] * scale,
@@ -358,14 +352,16 @@ def _chart_io(self, xoffset: int, y: int, port: int, pdata: tuple):
fill=self._set_col(pdata[i + 1]),
dash=DASH,
width=2,
+ tags=TAG_DATA,
)
- self._can_sysmon.create_line( # val
+ self._canvas.create_line( # val
x + cap,
y + p,
x + cap + pdata[i] * scale,
y + p,
fill=self._set_col(pdata[i]),
width=2,
+ tags=TAG_DATA,
)
p += 4
y += self._fonth
@@ -391,7 +387,19 @@ def _on_resize(self, event): # pylint: disable=unused-argument
"""
self.width, self.height = self.get_size()
- self._font, self._fonth = scale_font(self.width, 10, 35, 20)
+ self._font, _, self._fonth, _ = fitfont(
+ "X" * FONTSCALE, self.width, int(self.height / MAXLINES)
+ )
+ self._on_waiting()
+
+ def _on_waiting(self):
+ """
+ Display 'waiting for data' alert.
+ """
+
+ if self._waiting:
+ txt = DLGNOMONSYS if self._waits > MAXWAIT else DLGWAITMONSYS
+ self._canvas.create_alert(txt, tags=TAG_WAIT)
def get_size(self):
"""
@@ -402,4 +410,4 @@ def get_size(self):
"""
self.update_idletasks() # Make sure we know about any resizing
- return self._can_sysmon.winfo_width(), self._can_sysmon.winfo_height()
+ return self._canvas.winfo_width(), self._canvas.winfo_height()
diff --git a/src/pygpsclient/ubx_config_dialog.py b/src/pygpsclient/ubx_config_dialog.py
index 495b1fbf..4eb9aa96 100644
--- a/src/pygpsclient/ubx_config_dialog.py
+++ b/src/pygpsclient/ubx_config_dialog.py
@@ -88,9 +88,7 @@ def _body(self):
"""
# add configuration widgets
- self._frm_device_info = Hardware_Info_Frame(
- self.__app, self, borderwidth=2, relief="groove", protocol="UBX"
- )
+ self._frm_device_info = Hardware_Info_Frame(self.__app, self, protocol="UBX")
self._frm_config_port = UBX_PORT_Frame(
self.__app, self, borderwidth=2, relief="groove"
)
@@ -101,7 +99,7 @@ def _body(self):
self.__app, self, borderwidth=2, relief="groove"
)
self._frm_config_dynamic = Dynamic_Config_Frame(
- self.__app, self, borderwidth=2, relief="groove", protocol="UBX"
+ self.__app, self, protocol="UBX"
)
self._frm_configdb = UBX_CFGVAL_Frame(
self.__app, self, borderwidth=2, relief="groove"
diff --git a/src/pygpsclient/ubx_handler.py b/src/pygpsclient/ubx_handler.py
index 1e29747d..85616244 100644
--- a/src/pygpsclient/ubx_handler.py
+++ b/src/pygpsclient/ubx_handler.py
@@ -337,6 +337,10 @@ def _process_NAV_SAT(self, data: UBXMessage):
svid += 64
elev = getattr(data, "elev" + idx)
azim = getattr(data, "azim" + idx)
+ # elev = -91 normally means 'not used'
+ if not -90 <= elev <= 90:
+ elev = ""
+ azim = ""
cno = getattr(data, "cno" + idx)
self.__app.gnss_status.gsv_data[(gnssId, svid)] = (
gnssId,
diff --git a/src/pygpsclient/uni_handler.py b/src/pygpsclient/uni_handler.py
new file mode 100644
index 00000000..f0cddee1
--- /dev/null
+++ b/src/pygpsclient/uni_handler.py
@@ -0,0 +1,53 @@
+"""
+uni_handler.py
+
+WORK IN PROGRESS - AWAITING PARSER DEVELOPMENT
+
+Unicore GNSS Protocol handler - handles all incoming UNI messages
+
+Parses individual UNI (Unicore UM98n GNSS) messages (using pyunignss library)
+and adds selected attribute values to the app.gnss_status
+data dictionary. This dictionary is then used to periodically
+update the various user-selectable widget panels.
+
+Created on 27 Jan 2026
+
+:author: semuadmin (Steve Smith)
+:copyright: 2020 semuadmin
+:license: BSD 3-Clause
+"""
+
+import logging
+
+
+class UNIHandler:
+ """
+ UNIHandler class
+ """
+
+ def __init__(self, app):
+ """
+ Constructor.
+
+ :param Frame 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
+ self._parsed_data = None
+
+ # pylint: disable=unused-argument
+ def process_data(self, raw_data: bytes, parsed_data: object):
+ """
+ Process relevant UNI message types
+
+ :param bytes raw_data: raw data
+ :param UNIMessage parsed_data: parsed data
+ """
+
+ if raw_data is None:
+ pass
+ # self.logger.debug(f"data received {parsed_data.identity}")
diff --git a/src/pygpsclient/widget_state.py b/src/pygpsclient/widget_state.py
index 65ec6f54..e2c5727e 100644
--- a/src/pygpsclient/widget_state.py
+++ b/src/pygpsclient/widget_state.py
@@ -37,12 +37,14 @@ class definition and update `ubx_handler` to populate them.
COLSPAN = "colspan"
DEFAULT = "def"
+DOCK = "Dock"
HIDE = "Hide"
MAXCOLSPAN = 4 # max no of widget columns
MAXSPAN = 0 # always occupy the full row
MAXROWSPAN = 4 # max no of widget rows
RESET = "rst"
SHOW = "Show"
+UNDOCK = "Undock"
VISIBLE = "vis"
WDGCONSOLE = "Console"
WDGLEVELS = "Levels"
diff --git a/tests/test_configs.py b/tests/test_configs.py
index 783e54f4..ddf1bb88 100644
--- a/tests/test_configs.py
+++ b/tests/test_configs.py
@@ -19,15 +19,18 @@
config_disable_lc29h,
config_disable_lg290p,
config_disable_septentrio,
+ config_disable_unicore,
config_disable_ublox,
config_fixed_lc29h,
config_fixed_lg290p,
config_fixed_septentrio,
+ config_fixed_unicore,
config_fixed_ublox,
config_svin_lc29h,
config_svin_lg290p,
config_svin_quectel,
config_svin_septentrio,
+ config_svin_unicore,
config_svin_ublox,
)
@@ -59,6 +62,12 @@ def test_config_disable_septentrio(self):
res = config_disable_septentrio()
self.assertEqual(str(res), EXPECTED_RESULT)
+ def test_config_disable_unicore(self):
+ EXPECTED_RESULT = "['mode rover survey\\r\\n', 'rtcm1006 com1 0\\r\\n', 'rtcm1033 com1 0\\r\\n', 'rtcm1074 com1 0\\r\\n', 'rtcm1084 com1 0\\r\\n', 'rtcm1094 com1 0\\r\\n', 'rtcm1124 com1 0\\r\\n', 'saveconfig\\r\\n']"
+ res = config_disable_unicore()
+ # print(res)
+ self.assertEqual(str(res), EXPECTED_RESULT)
+
def test_config_disable_ublox(self):
EXPECTED_RESULT = ""
res = config_disable_ublox()
@@ -79,6 +88,12 @@ def test_config_fixed_septentrio(self):
res = config_fixed_septentrio(3000, 37.23345, -115.81513, 15.00, "LLH")
self.assertEqual(str(res), EXPECTED_RESULT)
+ def test_config_fixed_unicore(self):
+ EXPECTED_RESULT = "['mode base 37.23345 -115.81513 15.0\\r\\n', 'rtcm1006 com1 10\\r\\n', 'rtcm1033 com1 10\\r\\n', 'rtcm1074 com1 1\\r\\n', 'rtcm1084 com1 1\\r\\n', 'rtcm1094 com1 1\\r\\n', 'rtcm1124 com1 1\\r\\n', 'saveconfig\\r\\n']"
+ res = config_fixed_unicore(3000, 37.23345, -115.81513, 15.00, "LLH")
+ # print(res)
+ self.assertEqual(str(res), EXPECTED_RESULT)
+
def test_config_fixed_ublox(self):
EXPECTED_RESULT = ""
res = config_fixed_ublox(3000, 37.23345, -115.81513, 15.00, "LLH")
@@ -106,6 +121,12 @@ def test_config_svin_septentrio(self):
res = config_svin_septentrio(3000, 60)
self.assertEqual(str(res), EXPECTED_RESULT)
+ def test_config_svin_unicore(self):
+ EXPECTED_RESULT = "['mode base time 60\\r\\n', 'rtcm1006 com1 10\\r\\n', 'rtcm1033 com1 10\\r\\n', 'rtcm1074 com1 1\\r\\n', 'rtcm1084 com1 1\\r\\n', 'rtcm1094 com1 1\\r\\n', 'rtcm1124 com1 1\\r\\n', 'saveconfig\\r\\n']"
+ res = config_svin_unicore(3000, 60)
+ # print(res)
+ self.assertEqual(str(res), EXPECTED_RESULT)
+
def test_config_svin_ublox(self):
EXPECTED_RESULT = ""
res = config_svin_ublox(3000, 60)
diff --git a/tests/test_static.py b/tests/test_static.py
index 1a544efb..ac8089ee 100644
--- a/tests/test_static.py
+++ b/tests/test_static.py
@@ -19,6 +19,7 @@
from pyubx2 import UBXMessage, UBXReader
from pygpsclient.configuration import Configuration, INITMARKER
+from pygpsclient.gnss_status import GNSSStatus
from pygpsclient.globals import (
Area,
AreaXY,
@@ -74,6 +75,7 @@
ubx2preset,
unused_sats,
val2sphp,
+ valid_geom,
wnotow2date,
xy2ll,
)
@@ -124,6 +126,15 @@ def setUp(self):
def tearDown(self):
pass
+ def testgnssstatus(self):
+
+ gnss = GNSSStatus()
+ self.assertEqual(gnss.lat, 0.0)
+ self.assertEqual(gnss.gsv_data, {})
+ gnss.lat = 53
+ gnss.reset()
+ self.assertEqual(gnss.lat, 0.0)
+
def testconfiguration(self):
cfg = Configuration(DummyApp())
@@ -131,7 +142,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), 155)
+ self.assertEqual(len(cfg.settings), 156)
kwargs = {"userport": "/dev/ttyACM0", "spartnport": "/dev/ttyACM1"}
cfg.loadcli(**kwargs)
self.assertEqual(cfg.get("userport_s"), "/dev/ttyACM0")
@@ -404,7 +415,7 @@ def testwnotow2date(self):
]
for i, (wno, tow) in enumerate(vals):
self.assertEqual(str(wnotow2date(wno, tow)), dats[i])
- (wno, tow) = date2wnotow(datetime(2020, 4, 12))
+ wno, tow = date2wnotow(datetime(2020, 4, 12))
self.assertEqual(wnotow2date(wno, tow), datetime(2020, 4, 12))
def testbitsval(self):
@@ -629,10 +640,10 @@ def testareainbounds(self):
def testll2xy(self):
bounds = Area(53, -2, 54, -1)
- (x, y) = ll2xy(600, 400, bounds, Point(53.5, -1.5))
+ x, y = ll2xy(600, 400, bounds, Point(53.5, -1.5))
self.assertEqual(x, 300, 5)
self.assertEqual(y, 200, 5)
- (x, y) = ll2xy(600, 400, bounds, Point(53.52345, -1.81264))
+ x, y = ll2xy(600, 400, bounds, Point(53.52345, -1.81264))
self.assertAlmostEqual(x, 112.416, 5)
self.assertAlmostEqual(y, 190.620, 5)
@@ -940,6 +951,28 @@ def testunusedsats(self):
}
self.assertEqual(unused_sats(gsv_data), 0)
+ def testvalidgeom(self): # test valid_geom()
+ GEOMS = [
+ ("1064x620+30+45", True),
+ ("1064x0+30+45", False),
+ ("1064x620+-1544+45", True),
+ ("1064x620+-1544+-45", True),
+ ("0x245+0+45", False),
+ ("456x245+0+45", True),
+ ("01064x620+30+45", False),
+ ("1064x0620+30+45", False),
+ ("1064x620+0+0", True),
+ ("10x20+30+45", True),
+ ("56x07+1+01", False),
+ ("asdfxasdf+adf+45", False),
+ ("x5+3++", False),
+ ("2+2+2++", False),
+ ("", True),
+ ("asdf&%££asdf!!00#", False),
+ ]
+ for geom in GEOMS:
+ self.assertEqual(valid_geom(geom[0]), geom[1])
+
if __name__ == "__main__":
# import sys;sys.argv = ['', 'Test.testName']