diff --git a/README.md b/README.md
index 6307b3e7..385852fb 100644
--- a/README.md
+++ b/README.md
@@ -177,7 +177,7 @@ For more comprehensive installation instructions, please refer to [INSTALLATION.
#### GUI refresh rate setting
-30. PyGPSClient processes all incoming GNSS data in 'real time' but, by default, the GUI is only refreshed every 0.5 seconds. The refresh rate can be manually configured via the `guiupdateinterval_f` setting in the json configuration file. **NB:** PyGPSClient may become unresponsive on slower platforms (e.g. Raspberry Pi) at high message rates if the GUI update interval is less than 0.2 seconds.
+30. PyGPSClient processes all incoming GNSS data in 'real time' but, by default, the GUI is only refreshed every 0.5 seconds. The refresh rate can be manually configured via the `guiupdateinterval_f` setting in the json configuration file or Menu..Options..GUI Configuration dialog (*save configuration and restart after any changes*). **NB:** PyGPSClient may become unresponsive on slower platforms (e.g. Raspberry Pi) at high message rates if the GUI update interval is less than 0.2 seconds.
#### Toplevel ('pop-up') dialog setting
@@ -185,6 +185,7 @@ For more comprehensive installation instructions, please refer to [INSTALLATION.
- A boolean configuration setting `transient_dialog_b` governs whether Toplevel dialogs are 'transient' (i.e. always on top of main application dialog) or not. Changing this setting to `0` allows Toplevel dialogs to be minimised independently of the main application window, but be mindful that some dialogs may end up hidden behind others e.g. "Open file/folder" dialogs. **If a file open button appears unresponsive, check that the "Open file/folder" panel isn't already open but obscured**.
- If you're accessing the desktop via a VNC session (e.g. to a headless Raspberry Pi) it is recommended to keep the setting at the default `1`, as VNC may not recognise keystrokes on overlaid non-transient windows.
- A boolean configuration setting `resizeable_dialog_b` governs whether *all* Toplevel dialogs are resizeable, irrespective of the default setting in `DialogState`. Setting this to '1' provides a workaround for issues with some scaled Linux Wayland displays.
+ - Settings can be updated via the Menu..Options..GUI Configuration dialog (*save configuration and restart after any changes*).
#### User-selectable widgets
---
@@ -235,11 +236,9 @@ For more comprehensive installation instructions, please refer to [INSTALLATION.
**Legacy UBX:**
1. Version panel shows current device hardware/firmware versions (*Double-left-click to refresh*).
-1. Protocol Configuration panel (CFG-PRT) sets baud rate and inbound/outbound protocols across all available ports (*legacy protocols only*).
-1. Solution Rate panel (CFG-RATE) sets navigation solution interval in ms (e.g. 1000 = 1/second) and measurement ratio (ratio between the number of measurements and the number of navigation solutions, e.g. 5 = five measurements per navigation solution) (*legacy protocols only*).
-1. For each of the panels above, clicking anywhere in the panel background will refresh the displayed information with the current configuration.
-1. Message Rate panel (CFG-MSG) sets message rates per port for UBX and NMEA messages (*legacy protocols only*). Message rate is relative to navigation solution frequency e.g. a message rate of '4' means 'every 4th navigation solution' (higher = less frequent).
-1. UBX Legacy Command configuration panel providing structured updates for a range of legacy CFG-* configuration commands for *legacy protocols only*. Note: 'X' (byte) type attributes can be entered as integers or hexadecimal strings e.g. 522125312 or 0x1f1f0000. Once a command is selected, the configuration is polled and the current values displayed. The user can then amend these values as required and send the updated configuration. Some polls require input arguments (e.g. portID) - these are highlighted and will be set at default values initially (e.g. portID = 0), but can be amended by the user and re-polled using the  button.
+1. Protocol Configuration panel (CFG-PRT) sets baud rate and inbound/outbound protocols across all available ports. Clicking anywhere in the panel background will refresh the displayed information with the current configuration.
+1. Message Rate panel (CFG-MSG) sets message rates per port for UBX and NMEA messages. Message rate is relative to navigation solution frequency e.g. a message rate of '4' means 'every 4th navigation solution' (higher = less frequent).
+1. UBX Legacy Command configuration panel providing structured updates for a range of legacy CFG-* configuration commands. Note: 'X' (byte) type attributes can be entered as integers or hexadecimal strings e.g. 522125312 or 0x1f1f0000. Once a command is selected, the configuration is polled and the current values displayed. The user can then amend these values as required and send the updated configuration. Some polls require input arguments (e.g. portID) - these are highlighted and will be set at default values initially (e.g. portID = 0), but can be amended by the user and re-polled using the  button.
---
## NMEA Configuration Facilities
@@ -454,12 +453,12 @@ facility is *not* intended to be used for real time navigational purposes.
Once you have received the API key (a 32-character alphanumeric string), you can (in order of precedence):
-1. Copy it to the `"mqapikey_s":` value in your json configuration file (see example provided).
+1. Copy it to the `"mqapikey_s":` value in your json configuration file (see example provided), or via the Menu..Options..GUI Configuration dialog.
2. Create an environment variable named `MQAPIKEY` (all upper case) and set this to the API key value. It is recommended
that this is a User variable rather than a System/Global variable.
3. Pass it via command line argument `--mqapikey`.
-*The web map refresh rate can be amended if required by changing the `mapupdateinterval_n:` value in your json configuration file.
+\*The web map refresh rate can be amended if required by changing the `mapupdateinterval_n:` value in your json configuration file, or via the Menu..Options..GUI Configuration Options dialog (*save configuration and restart after any changes*).
---
## User Defined Presets
@@ -514,7 +513,7 @@ For further details, refer to the `pygnssutils` homepage at [https://github.com/
1. There is a known issue with PyGPSClient GUI refreshes becoming progressively slower on certain platforms if the app is left unattended (_i.e. no user interaction_) for an extended period - typically 30 minutes or more. The issue is more pronounced on low-end SBC platforms like the Raspberry Pi. **Underlying processing (including message parsing and datalogging) is unaffected**, and the GUI can generally be 'woken up' within a few seconds via a simple user interaction e.g. resizing the main panel. The root cause of this issue is under investigation, but as a workaround, users can try a) increasing the `guiupdateinterval_f` setting in the json configuration file, or b) hiding some or all user-selectable widgets until needed.
-2. **NB:** The latest version of Python for MacOS (>=3.14.5) comes with a new version of tkinter (9.0). There appear to be fairly serious performance issues with this version on MacOS Tahoe which render the PyGPSClient GUI somewhat sluggish. For the time being, it is recommended that users use >=3.14.4. This issue does *not* affect other operating systems or Python apps not using tkinter.
+2. **NB:** The latest version of Python for MacOS (>=3.14.5) comes with a new version of tkinter (9.0). Early iterations of this version (3.14.5, 3.14.6) displayed fairly serious performance issues on MacOS Sonoma & Tahoe. The issue appears to have been resolved in version >=3.14.7 (tkinter 9.0.4), but if you experience any compatibility or performance issues with tkinter 9.0, consider reverting to Python <=3.14.4. This issue does *not* affect other operating systems or Python apps not using tkinter.
3. If you encounter persistent `WARNING>>Error parsing data stream Serial stream terminated unexpectedly` messages in the console, this may be indicative of insufficient serial port bandwidth (baudrate or timeout) for the current output message cohort (*particularly if this includes raw Ephemerides or Observation data*). Try increasing the baudrate in the first instance.
diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md
index e4a02d4c..8fefd9a5 100644
--- a/RELEASE_NOTES.md
+++ b/RELEASE_NOTES.md
@@ -1,5 +1,14 @@
# PyGPSClient Release Notes
+### RELEASE 1.7.4
+
+ENHANCEMENTS:
+
+1. Add GUI Configuration Options panel to set internal GUI configuration parameters (*previously only available via manual edits of json file*). **NB**: User must save configuration and restart app after any changes.
+1. Add "Toggle Settings" button to Banner panel for quick access to Settings (docked or undocked).
+1. Streamline UBX Legacy Configuration panel for improved access on low res displays.
+1. Enhanced validation messaging in UBX and NMEA configuration panels.
+
### RELEASE 1.7.3
FIXES:
diff --git a/docs/pygpsclient.rst b/docs/pygpsclient.rst
index 650f0e38..1fd6b817 100644
--- a/docs/pygpsclient.rst
+++ b/docs/pygpsclient.rst
@@ -132,6 +132,14 @@ pygpsclient.gpx\_dialog module
:undoc-members:
:show-inheritance:
+pygpsclient.gui\_config\_dialog module
+--------------------------------------
+
+.. automodule:: pygpsclient.gui_config_dialog
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
pygpsclient.hardware\_info\_frame module
----------------------------------------
@@ -500,14 +508,6 @@ pygpsclient.ubx\_preset\_frame module
:undoc-members:
:show-inheritance:
-pygpsclient.ubx\_solrate\_frame module
---------------------------------------
-
-.. automodule:: pygpsclient.ubx_solrate_frame
- :members:
- :undoc-members:
- :show-inheritance:
-
pygpsclient.uni\_handler module
-------------------------------
diff --git a/images/ubxconfiglegacy_widget.png b/images/ubxconfiglegacy_widget.png
index c1296c3e..a09ef59f 100644
Binary files a/images/ubxconfiglegacy_widget.png and b/images/ubxconfiglegacy_widget.png differ
diff --git a/pyproject.toml b/pyproject.toml
index 0c6fa897..0fdd2087 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -54,7 +54,7 @@ dependencies = [
"Pillow>=12.0.0",
"pygnssutils>=1.2.7",
"pyunigps>=1.0.0",
- "pynmeagps>=1.1.5",
+ "pynmeagps>=1.1.7",
"pyubx2>=1.3.5",
]
diff --git a/src/pygpsclient/_version.py b/src/pygpsclient/_version.py
index d1a925ac..0fd72f84 100644
--- a/src/pygpsclient/_version.py
+++ b/src/pygpsclient/_version.py
@@ -8,4 +8,4 @@
:license: BSD 3-Clause
"""
-__version__ = "1.7.3"
+__version__ = "1.7.4"
diff --git a/src/pygpsclient/about_dialog.py b/src/pygpsclient/about_dialog.py
index 4e74ee2f..13430c3b 100644
--- a/src/pygpsclient/about_dialog.py
+++ b/src/pygpsclient/about_dialog.py
@@ -12,7 +12,18 @@
import logging
from platform import machine, python_version
-from tkinter import Button, Checkbutton, Frame, IntVar, Label, Tcl
+from tkinter import (
+ CENTER,
+ EW,
+ NSEW,
+ Button,
+ Checkbutton,
+ Frame,
+ IntVar,
+ Label,
+ Tcl,
+ ttk,
+)
from webbrowser import open_new_tab
from PIL import Image, ImageTk
@@ -21,7 +32,6 @@
CLICK_CURSOR,
ERRCOL,
ICON_APP128,
- ICON_GITHUB,
ICON_SPONSOR,
INFOCOL,
LICENSE_URL,
@@ -61,7 +71,6 @@ def __init__(self, app, *args, **kwargs): # pylint: disable=unused-argument
self.__app = app # Reference to main application class
self.logger = logging.getLogger(__name__)
self._img_icon = ImageTk.PhotoImage(Image.open(ICON_APP128).resize((64, 64)))
- self._img_github = ImageTk.PhotoImage(Image.open(ICON_GITHUB).resize((32, 32)))
self._img_sponsor = ImageTk.PhotoImage(Image.open(ICON_SPONSOR))
self._checkonstartup = IntVar()
self._checkonstartup.set(self.__app.configuration.get("checkforupdate_b"))
@@ -79,16 +88,23 @@ def _body(self):
"""
self._frm_body = Frame(self.container)
- self._lbl_icon = Label(self._frm_body, image=self._img_icon, borderwidth=0)
- self._lbl_descs = []
- for txt in ABOUTTXT:
- self._lbl_descs.append(
- Label(
- self._frm_body,
- text=txt,
- borderwidth=0,
- )
- )
+ self._lbl_icon = Label(
+ self._frm_body, image=self._img_icon, borderwidth=0, anchor=CENTER
+ )
+ self._lbl_desc = Label(
+ self._frm_body,
+ text=ABOUTTXT,
+ wraplength=400,
+ justify=CENTER,
+ anchor=CENTER,
+ )
+ self._lbl_github = Label(
+ self._frm_body,
+ text=GITHUB_URL,
+ foreground=INFOCOL,
+ cursor=CLICK_CURSOR,
+ anchor=CENTER,
+ )
tkv = Tcl().call("info", "patchlevel")
self._lbl_python_version = Label(
self._frm_body,
@@ -97,6 +113,7 @@ def _body(self):
f"Python: {python_version()} Tk: {tkv} "
f"Spatial: {SQLSTATUS[self.__app.db_enabled]}"
),
+ anchor=CENTER,
)
self._lbl_lib_versions = []
for nam, ver in LIBVERSIONS.items():
@@ -104,7 +121,8 @@ def _body(self):
Label(
self._frm_body,
text=f"{nam}: {ver}",
- borderwidth=0,
+ anchor=CENTER,
+ border=0,
highlightthickness=0,
)
)
@@ -123,17 +141,14 @@ def _body(self):
self._frm_body,
image=self._img_sponsor,
cursor=CLICK_CURSOR,
- )
- self._lbl_github = Label(
- self._frm_body,
- text=GITHUB_URL,
- fg=INFOCOL,
- cursor=CLICK_CURSOR,
+ anchor=CENTER,
)
self._lbl_copyright = Label(
self._frm_body,
text=COPYRIGHT,
+ foreground=INFOCOL,
cursor=CLICK_CURSOR,
+ anchor=CENTER,
)
def _do_layout(self):
@@ -141,26 +156,34 @@ def _do_layout(self):
Arrange widgets in dialog.
"""
- i = 0
- self._frm_body.grid(column=0, row=0, padx=5, pady=5, ipadx=5, ipady=5)
- self._lbl_icon.grid(column=0, row=1, columnspan=2, padx=3, pady=0)
- for i, lbl in enumerate(self._lbl_descs):
- lbl.grid(column=0, row=2 + i, columnspan=2, padx=3, pady=0)
- self._lbl_python_version.grid(column=0, row=3 + i, columnspan=2, padx=3, pady=1)
- n = 4 + i
+ self._frm_body.grid(column=0, row=0, ipadx=2, ipady=2, sticky=NSEW)
+ self._lbl_icon.grid(column=0, row=0, columnspan=2, padx=3, pady=0, sticky=EW)
+ self._lbl_desc.grid(column=0, row=1, columnspan=2, padx=3, pady=0, sticky=EW)
+ self._lbl_github.grid(column=0, row=2, columnspan=2, padx=3, pady=0, sticky=EW)
+ ttk.Separator(self._frm_body).grid(
+ column=0, row=3, columnspan=2, padx=3, pady=3, sticky=EW
+ )
+ self._lbl_python_version.grid(
+ column=0, row=4, columnspan=2, padx=3, pady=1, sticky=EW
+ )
for i, lbl in enumerate(self._lbl_lib_versions):
- lbl.grid(column=0, row=n + i, columnspan=2, padx=2)
+ lbl.grid(column=0, row=5 + i, columnspan=2, padx=2, pady=0, sticky=EW)
+ lv = len(self._lbl_lib_versions)
self._btn_checkupdate.grid(
- column=0, row=1 + n + i, ipadx=3, ipady=3, padx=3, pady=3
+ column=0, row=6 + lv, ipadx=3, ipady=3, padx=3, pady=3
)
self._chk_checkupdate.grid(
- column=1, row=1 + n + i, ipadx=3, ipady=3, padx=3, pady=3
+ column=1, row=6 + lv, ipadx=3, ipady=3, padx=3, pady=3
+ )
+ ttk.Separator(self._frm_body).grid(
+ column=0, row=7 + lv, columnspan=2, padx=3, pady=3, sticky=EW
)
self._lbl_sponsoricon.grid(
- column=0, row=2 + n + i, columnspan=2, padx=3, pady=3
+ column=0, row=8 + lv, columnspan=2, padx=3, pady=3, sticky=EW
+ )
+ self._lbl_copyright.grid(
+ column=0, row=9 + lv, columnspan=2, padx=3, pady=3, sticky=EW
)
- self._lbl_github.grid(column=0, row=3 + n + i, columnspan=2, padx=3, pady=0)
- self._lbl_copyright.grid(column=0, row=4 + n + i, columnspan=2, padx=3, pady=0)
def _attach_events(self):
"""
@@ -238,7 +261,7 @@ def _check_for_update(self, *args, **kwargs): # pylint: disable=unused-argument
txt += f" - Latest version is {latest}"
col = ERRCOL
self._lbl_lib_versions[i]["text"] = txt
- self._lbl_lib_versions[i]["fg"] = col
+ self._lbl_lib_versions[i]["foreground"] = col
updates = [nam for (nam, current, latest) in versions if latest != current]
if len(updates) > 0:
self.set_status_label("Updates available", OKCOL)
@@ -272,10 +295,10 @@ def _set_update_btn_mode(self, update: bool):
if update:
self._btn_checkupdate["text"] = "UPDATE"
- self._btn_checkupdate["fg"] = OKCOL
+ self._btn_checkupdate["foreground"] = OKCOL
self._btn_checkupdate.bind("", self._do_update)
else:
self._btn_checkupdate["text"] = "CHECK FOR UPDATES"
- self._btn_checkupdate["fg"] = INFOCOL
+ self._btn_checkupdate["foreground"] = INFOCOL
self._btn_checkupdate.bind("", self._check_for_update)
self.__app.update_idletasks()
diff --git a/src/pygpsclient/app.py b/src/pygpsclient/app.py
index 7cf9ec67..f0c7f937 100644
--- a/src/pygpsclient/app.py
+++ b/src/pygpsclient/app.py
@@ -323,7 +323,8 @@ def _do_layout(self):
# dynamically instantiate and position widgets in frm_widgets
col = 0
row = 0
- for i, (name, state) in enumerate(self.widget_state.state.items()):
+ i = 0
+ for name, state in self.widget_state.state.items():
# setup widget
frm = self.widget_setup(state)
# set corresponding View menu label to 'Show' or 'Hide'
@@ -345,6 +346,7 @@ def _do_layout(self):
row += 1
else:
col += c
+ i += 1
# set column and row weights to control 'pack' behaviour of main layout
self.grid_columnconfigure(0, weight=1)
@@ -509,9 +511,12 @@ def set_event(self, evt: str):
self.event_generate(evt)
- def load_config(self):
+ def load_config(self) -> str:
"""
Load configuration file menu option.
+
+ :return: error code
+ :rtype: str
"""
# Warn if Streaming, NTRIP or SPARTN clients are running
@@ -519,7 +524,7 @@ def load_config(self):
self.set_status_label("", OKCOL)
else:
self.set_status_label(DLGSTOPRTK, ERRCOL)
- return
+ return "cancelled"
_, err = self.configuration.loadfile()
if err == "": # load succeeded
@@ -533,10 +538,14 @@ def load_config(self):
self._do_layout()
elif err == "cancelled":
pass
+ return err
- def save_config(self):
+ def save_config(self) -> str:
"""
Save configuration file menu option.
+
+ :return: error code
+ :rtype: str
"""
# save current screen geometry
@@ -549,6 +558,7 @@ def save_config(self):
pass
else: # save failed
self.set_status_label(SAVECONFIGBAD.format(err), ERRCOL)
+ return err
def update_widgets(self):
"""
diff --git a/src/pygpsclient/banner_frame.py b/src/pygpsclient/banner_frame.py
index 762229c9..a53399b8 100644
--- a/src/pygpsclient/banner_frame.py
+++ b/src/pygpsclient/banner_frame.py
@@ -25,8 +25,6 @@
CONNECTED_FILE,
CONNECTED_NTRIP,
CONNECTED_SOCKET,
- CONNECTED_SPARTNIP,
- CONNECTED_SPARTNLB,
DMM,
DMS,
ECEF,
@@ -41,8 +39,8 @@
ICON_NOCLIENT,
ICON_NTRIPCONFIG,
ICON_SERIAL,
+ ICON_SETTINGS,
ICON_SOCKET,
- ICON_SPARTNCONFIG,
ICON_TRANSMIT,
UI,
UIK,
@@ -101,8 +99,8 @@ def __init__(self, app: Tk, *args, **kwargs):
self._img_transmit = ImageTk.PhotoImage(Image.open(ICON_TRANSMIT))
self._img_noclient = ImageTk.PhotoImage(Image.open(ICON_NOCLIENT))
self._img_ntrip = ImageTk.PhotoImage(Image.open(ICON_NTRIPCONFIG))
- self._img_spartn = ImageTk.PhotoImage(Image.open(ICON_SPARTNCONFIG))
self._img_blank = ImageTk.PhotoImage(Image.open(ICON_BLANK))
+ self._img_settings = ImageTk.PhotoImage(Image.open(ICON_SETTINGS))
self._sep = False
self._old_vms = 0
@@ -148,6 +146,14 @@ def _body(self):
self._lbl_lfix = Label(
self._frm_basic, text="fix:", bg=BGCOL, fg=FGCOL, anchor=N
)
+ self._btn_settings = Button(
+ self._frm_toggle,
+ width=28,
+ height=22,
+ command=self._toggle_settings,
+ image=self._img_settings,
+ cursor=CLICK_CURSOR,
+ )
self._btn_toggle = Button(
self._frm_toggle,
width=28,
@@ -297,7 +303,8 @@ def _do_layout(self):
self._lbl_diffcorr.grid(column=11, row=0, pady=0, padx=0, sticky=W)
self._lbl_diffstat.grid(column=12, row=0, pady=0, padx=0, sticky=W)
- self._btn_toggle.grid(column=0, row=0, padx=0, pady=0, sticky=NE)
+ self._btn_settings.grid(column=0, row=0, padx=4, pady=0, sticky=NE)
+ self._btn_toggle.grid(column=1, row=0, padx=0, pady=0, sticky=NE)
self._toggle_advanced()
@@ -330,6 +337,13 @@ def _toggle_advanced(self):
self._frm_advanced2.grid_forget()
self._btn_toggle["image"] = self._img_expand
+ def _toggle_settings(self):
+ """
+ Toggle settings panel on or off.
+ """
+
+ self.__app.settings_dock()
+
def update_conn_status(self, status: int):
"""
Update connection status icon
@@ -355,8 +369,6 @@ def update_rtk_status(self, rtk: int = 0):
if rtk == CONNECTED_NTRIP:
self._lbl_rtk_preset.configure(image=self._img_ntrip)
- elif rtk in (CONNECTED_SPARTNIP, CONNECTED_SPARTNLB):
- self._lbl_rtk_preset.configure(image=self._img_spartn)
else:
self._lbl_rtk_preset.configure(image=self._img_blank)
diff --git a/src/pygpsclient/configuration.py b/src/pygpsclient/configuration.py
index 3c8d8fc6..fe6665c3 100644
--- a/src/pygpsclient/configuration.py
+++ b/src/pygpsclient/configuration.py
@@ -102,7 +102,7 @@ def __init__(self, app):
"transient_dialog_b": 1, # whether pop-up dialogs are on top of main app window
"resizeable_dialog_b": 0, # whether pop-up dialogs are all resizeable
"guiupdateinterval_f": GUI_UPDATE_INTERVAL, # GUI widget update interval in seconds
- "mapupdateinterval_n": MAP_UPDATE_INTERVAL,
+ "mapupdateinterval_n": MAP_UPDATE_INTERVAL, # MAPQUEST map update interval in seconds
"defaultport_s": RCVR_CONNECTION,
"nmeaprot_b": 1,
"ubxprot_b": 1,
diff --git a/src/pygpsclient/dialog_state.py b/src/pygpsclient/dialog_state.py
index 791e70ae..960be7dd 100644
--- a/src/pygpsclient/dialog_state.py
+++ b/src/pygpsclient/dialog_state.py
@@ -18,6 +18,7 @@
from pygpsclient.about_dialog import AboutDialog
from pygpsclient.globals import CLASS, RESIZE
from pygpsclient.gpx_dialog import GPXViewerDialog
+from pygpsclient.gui_config_dialog import GUIConfigDialog
from pygpsclient.importmap_dialog import ImportMapDialog
from pygpsclient.nmea_config_dialog import NMEAConfigDialog
from pygpsclient.ntrip_client_dialog import NTRIPConfigDialog
@@ -27,6 +28,7 @@
from pygpsclient.settings_dialog import SettingsDialog
from pygpsclient.strings import (
DLG,
+ DLGGUIOPTIONS,
DLGTABOUT,
DLGTGPX,
DLGTIMPORTMAP,
@@ -69,7 +71,7 @@ def __init__(self):
DLGTUBXLEGACY: {
CLASS: UBXLegacyConfigDialog,
DLG: None,
- RESIZE: False,
+ RESIZE: True,
},
DLGTNMEA: {
CLASS: NMEAConfigDialog,
@@ -116,5 +118,10 @@ def __init__(self):
DLG: None,
RESIZE: False,
},
+ DLGGUIOPTIONS: {
+ CLASS: GUIConfigDialog,
+ DLG: None,
+ RESIZE: False,
+ },
# add any new dialogs here
}
diff --git a/src/pygpsclient/dynamic_config_frame.py b/src/pygpsclient/dynamic_config_frame.py
index 69d59428..b14e6326 100644
--- a/src/pygpsclient/dynamic_config_frame.py
+++ b/src/pygpsclient/dynamic_config_frame.py
@@ -335,7 +335,7 @@ def _on_set_cfg(self, *args, **kwargs): # pylint: disable=unused-argument
"""
if self._cfg_id in ("", None):
- self.__container.status_label = ("Select command", ERRCOL)
+ self.__container.set_status_label("Select command", ERRCOL)
return
nam = ""
@@ -367,15 +367,15 @@ def _on_set_cfg(self, *args, **kwargs): # pylint: disable=unused-argument
# send message, update status and await response
self.__container.send_command(msg)
self._lbl_send_command["image"] = self.__container.img_pending
- self.__container.status_label = f"P{self._cfg_id} SET message sent"
+ self.__container.set_status_label(f"P{self._cfg_id} SET message sent")
for msgid in pendcfg:
self.__container.set_pending(msgid, penddlg)
self._expected_response = SET
except ValueError as err:
self.logger.debug(traceback.format_exc())
- self.__container.status_label = (
- f"INVALID! {nam}, {att}: {err}",
+ self.__container.set_status_label(
+ f"Invalid - {nam}, {att}: {err}",
ERRCOL,
)
@@ -389,7 +389,7 @@ def _do_poll_cfg(self, *args, **kwargs): # pylint: disable=unused-argument
"""
if self._cfg_id in ("", None):
- self.__container.status_label = ("Select command", ERRCOL)
+ self.__container.set_status_label("Select command", ERRCOL)
return
msg = penddlg = pendcfg = None
@@ -414,13 +414,13 @@ def _do_poll_cfg(self, *args, **kwargs): # pylint: disable=unused-argument
cp = "P" if self._protocol == NMEA else ""
if msg is not None:
self.__container.send_command(msg)
- self.__container.status_label = f"{cp}{cfg_id} POLL message sent"
+ self.__container.set_status_label(f"{cp}{cfg_id} POLL message sent")
self._lbl_send_command["image"] = self.__container.img_pending
for msgid in pendcfg:
self.__container.set_pending(msgid, penddlg)
self._expected_response = POLL
else: # CFG cannot be POLLed
- self.__container.status_label = f"{cp}{cfg_id} No POLL available"
+ self.__container.set_status_label(f"{cp}{cfg_id} No POLL available")
self._lbl_send_command["image"] = self.__container.img_unknown
def _do_poll_args(self, cfg_id: str) -> dict:
@@ -499,13 +499,13 @@ def update_status(self, msg: NMEAMessage | UBXMessage):
self._update_widgets(msg)
if ok:
- self.__container.status_label = (
+ self.__container.set_status_label(
f"{cfg_id} message acknowledged",
OKCOL,
)
self._lbl_send_command["image"] = self.__container.img_confirmed
else:
- self.__container.status_label = (f"{cfg_id} message rejected", ERRCOL)
+ self.__container.set_status_label(f"{cfg_id} message rejected", ERRCOL)
self._lbl_send_command["image"] = self.__container.img_warn
self.update()
diff --git a/src/pygpsclient/globals.py b/src/pygpsclient/globals.py
index 024efe9d..891c46c0 100644
--- a/src/pygpsclient/globals.py
+++ b/src/pygpsclient/globals.py
@@ -175,10 +175,11 @@
ICON_REFRESH = path.join(DIRNAME, "resources/iconmonstr-refresh-6-16.png")
ICON_RIGHT = path.join(DIRNAME, "resources/iconmonstr-caret-right-filled-32.png")
ICON_SAVE = path.join(DIRNAME, "resources/iconmonstr-save-14-24.png")
+ICON_SAVE_DISABLED = path.join(DIRNAME, "resources/iconmonstr-save-disabled-24.png")
ICON_SEND = path.join(DIRNAME, "resources/iconmonstr-arrow-12-24.png")
ICON_SERIAL = path.join(DIRNAME, "resources/usbport-1-24.png")
+ICON_SETTINGS = path.join(DIRNAME, "resources/iconmonstr-gear-2-24.png")
ICON_SOCKET = path.join(DIRNAME, "resources/ethernet-1-24.png")
-ICON_SPARTNCONFIG = path.join(DIRNAME, "resources/iconmonstr-antenna-3-24.png")
ICON_SPONSOR = path.join(DIRNAME, "resources/bmac-logo-60.png")
ICON_START = path.join(DIRNAME, "resources/marker_start.png")
ICON_STOP = path.join(DIRNAME, "resources/iconmonstr-stop-1-24.png")
@@ -282,6 +283,8 @@
VALBOOL = 10
VALREGEX = 11
VALCUSTOM = 12
+VALSIGNED = 13
+VALUNSIGNED = 14
WAYPOINT = "waypoint"
WIDGETU1 = (200, 200) # small widget size
WIDGETU2 = (300, 200) # medium widget size
diff --git a/src/pygpsclient/gui_config_dialog.py b/src/pygpsclient/gui_config_dialog.py
new file mode 100644
index 00000000..882fc7a7
--- /dev/null
+++ b/src/pygpsclient/gui_config_dialog.py
@@ -0,0 +1,278 @@
+"""
+gui_config_dialog.py
+
+GUI Configuration setting dialog.
+
+Created on 27 Aug 2026
+
+:author: semuadmin (Steve Smith)
+:copyright: 2020 semuadmin
+:license: BSD 3-Clause
+"""
+
+# pylint: disable=unused-argument
+
+from tkinter import (
+ DISABLED,
+ EW,
+ NORMAL,
+ NSEW,
+ BooleanVar,
+ Button,
+ Checkbutton,
+ E,
+ Entry,
+ Frame,
+ Label,
+ Spinbox,
+ StringVar,
+ Tk,
+ W,
+)
+
+from PIL import Image, ImageTk
+
+from pygpsclient.globals import (
+ CLICK_CURSOR,
+ ERRCOL,
+ ICON_SAVE,
+ ICON_SAVE_DISABLED,
+ INFOCOL,
+ OKCOL,
+ READONLY,
+ TRACEMODE_WRITE,
+)
+from pygpsclient.helpers import trace_update # pylint: disable=unused-import
+from pygpsclient.strings import DLGGUIOPTIONS
+from pygpsclient.toplevel_dialog import ToplevelDialog
+
+GUI_INTERVALS = (
+ "100",
+ "200",
+ "300",
+ "400",
+ "500",
+ "750",
+ "1000",
+ "2000",
+ "5000",
+ "10000",
+)
+MAP_INTERVALS = ("1", "2", "5", "10", "30", "60", "120", "240", "360")
+
+
+class GUIConfigDialog(ToplevelDialog):
+ """
+ GUI Configuration Options panel.
+ """
+
+ def __init__(self, app: Tk, *args, **kwargs):
+ """
+ Constructor.
+
+ :param Tk app: reference to main tkinter application
+ :param args: optional args to pass to Frame parent class
+ :param kwargs: optional kwargs to pass to Frame parent class
+ """
+
+ self.__app = app
+ super().__init__(app, DLGGUIOPTIONS)
+ self.width = int(kwargs.get("width", 600))
+ self.height = int(kwargs.get("height", 300))
+ self._img_save = ImageTk.PhotoImage(Image.open(ICON_SAVE))
+ self._img_save_disabled = ImageTk.PhotoImage(Image.open(ICON_SAVE_DISABLED))
+ self._guirefresh = StringVar()
+ self._maprefresh = StringVar()
+ self._mapkey = StringVar()
+ self._resizedialog = BooleanVar()
+ self._transientdialog = BooleanVar()
+
+ self._body()
+ self._do_layout()
+ self._reset()
+ self._attach_events()
+ self._finalise()
+
+ def _body(self):
+ """
+ Set up frame and widgets.
+ """
+
+ self._frm_body = Frame(self.container)
+ self._lbl_guirefresh = Label(
+ self._frm_body, text="GUI Refresh Interval", anchor=W
+ )
+ self._lbl_guirefreshu = Label(self._frm_body, text="ms", anchor=W)
+ self._spn_guirefresh = Spinbox(
+ self._frm_body,
+ values=GUI_INTERVALS,
+ width=6,
+ state=READONLY,
+ wrap=True,
+ textvariable=self._guirefresh,
+ )
+ self._lbl_maprefresh = Label(
+ self._frm_body, text="MapQuest Refresh Interval", anchor=W
+ )
+ self._lbl_maprefreshu = Label(self._frm_body, text="s", anchor=W)
+ self._spn_maprefresh = Spinbox(
+ self._frm_body,
+ values=MAP_INTERVALS,
+ width=6,
+ state=READONLY,
+ wrap=True,
+ textvariable=self._maprefresh,
+ )
+ self._lbl_mapkey = Label(self._frm_body, text="MapQuest API Key", anchor=W)
+ self._ent_mapkey = Entry(
+ self._frm_body,
+ width=35,
+ textvariable=self._mapkey,
+ )
+ self._lbl_resizedialog = Label(
+ self._frm_body, text="Resizeable TopLevel Dialogs", anchor=W
+ )
+ self._chk_resizedialog = Checkbutton(
+ self._frm_body,
+ text="",
+ variable=self._resizedialog,
+ )
+ self._lbl_transientdialog = Label(
+ self._frm_body, text="Transient TopLevel Dialogs", anchor=W
+ )
+ self._chk_transientdialog = Checkbutton(
+ self._frm_body,
+ text="",
+ variable=self._transientdialog,
+ )
+ self._btn_save = Button(
+ self._frm_body,
+ command=self._on_save_updates,
+ image=self._img_save_disabled,
+ width=45,
+ )
+
+ def _do_layout(self):
+ """
+ Layout widgets.
+ """
+
+ self._frm_body.grid(column=0, row=0, sticky=NSEW)
+ self._lbl_guirefresh.grid(column=0, row=0, sticky=W)
+ self._spn_guirefresh.grid(column=1, row=0, sticky=W)
+ self._lbl_guirefreshu.grid(column=2, row=0, padx=2, sticky=W)
+ self._lbl_maprefresh.grid(column=0, row=1, sticky=W)
+ self._spn_maprefresh.grid(column=1, row=1, sticky=W)
+ self._lbl_maprefreshu.grid(column=2, row=1, padx=2, sticky=W)
+ self._lbl_mapkey.grid(column=0, row=2, sticky=W)
+ self._ent_mapkey.grid(column=1, row=2, columnspan=2, sticky=EW)
+ self._lbl_resizedialog.grid(column=0, row=3, sticky=W)
+ self._chk_resizedialog.grid(column=1, row=3, columnspan=2, sticky=W)
+ self._lbl_transientdialog.grid(column=0, row=4, sticky=W)
+ self._chk_transientdialog.grid(column=1, row=4, columnspan=2, sticky=W)
+ self._btn_save.grid(column=1, row=5, columnspan=2, padx=4, pady=3, sticky=E)
+ self._frm_body.columnconfigure(2, weight=1)
+
+ def _attach_events(self):
+ """
+ Bind events to window.
+ """
+
+ self.container.bind("", self._on_resize)
+ for var in (
+ self._guirefresh,
+ self._maprefresh,
+ self._resizedialog,
+ self._transientdialog,
+ self._mapkey,
+ ):
+ var.trace_update(TRACEMODE_WRITE, self._on_update, True)
+
+ def _reset(self):
+ """
+ Reset panel to initial settings
+ """
+
+ self._guirefresh.set(
+ int(self.__app.configuration.get("guiupdateinterval_f") * 1000)
+ )
+ self._maprefresh.set(self.__app.configuration.get("mapupdateinterval_n"))
+ self._mapkey.set(self.__app.configuration.get("mqapikey_s"))
+ self._resizedialog.set(int(self.__app.configuration.get("resizeable_dialog_b")))
+ self._transientdialog.set(
+ int(self.__app.configuration.get("transient_dialog_b"))
+ )
+ self._enable_save_button(False)
+ self.set_status_label("Update with caution! Restart after updating", ERRCOL)
+
+ def _on_update(self, var, index, mode):
+ """
+ Setting has been updated.
+ """
+
+ updates = (
+ (
+ self.__app.configuration.get("guiupdateinterval_f")
+ != float(self._guirefresh.get()) / 1000
+ )
+ | (
+ self.__app.configuration.get("mapupdateinterval_n")
+ != int(self._maprefresh.get())
+ )
+ | (self.__app.configuration.get("mqapikey_s") != self._mapkey.get())
+ | (
+ self.__app.configuration.get("resizeable_dialog_b")
+ != int(self._resizedialog.get())
+ )
+ | (
+ self.__app.configuration.get("transient_dialog_b")
+ != int(self._transientdialog.get())
+ )
+ )
+ msg = "Settings updated" if updates else ""
+ if int(self._maprefresh.get()) < 60:
+ msg += ". Check MapQuest Fees!"
+ self._enable_save_button(updates)
+ self.set_status_label(msg, INFOCOL)
+
+ def _enable_save_button(self, enabled: bool):
+ """
+ Enable or disable save button.
+
+ :param bool enabled: enabled state
+ """
+
+ if enabled:
+ state = NORMAL
+ cursor = CLICK_CURSOR
+ image = self._img_save
+ else:
+ state = DISABLED
+ cursor = ""
+ image = self._img_save_disabled
+ self._btn_save["state"] = state
+ self._btn_save["cursor"] = cursor
+ self._btn_save["image"] = image
+
+ def _on_save_updates(self):
+ """
+ Save updated values to configuration file.
+ """
+
+ self.__app.configuration.set(
+ "guiupdateinterval_f", float(self._guirefresh.get()) / 1000
+ )
+ self.__app.configuration.set("mapupdateinterval_n", int(self._maprefresh.get()))
+ self.__app.configuration.set("mqapikey_s", self._mapkey.get())
+ self.__app.configuration.set(
+ "resizeable_dialog_b", int(self._resizedialog.get())
+ )
+ self.__app.configuration.set(
+ "transient_dialog_b", int(self._transientdialog.get())
+ )
+ self.set_status_label("Save configuration to file and restart", OKCOL)
+ if self.__app.save_config() != "":
+ self.set_status_label("Save cancelled", INFOCOL)
+ else:
+ self.set_status_label("Restart app", OKCOL)
+ self._enable_save_button(False)
diff --git a/src/pygpsclient/hardware_info_frame.py b/src/pygpsclient/hardware_info_frame.py
index 1cad4af4..8901d6d2 100644
--- a/src/pygpsclient/hardware_info_frame.py
+++ b/src/pygpsclient/hardware_info_frame.py
@@ -105,7 +105,7 @@ def repoll(self, event=None): # pylint: disable=unused-argument):
"""
self.__app.poll_version(self.__app.protocol_mask)
- self.__container.status_label = (HWREPOLL, INFOCOL)
+ self.__container.set_status_label(HWREPOLL, INFOCOL)
def reset(self, event=None): # pylint: disable=unused-argument):):
"""
@@ -125,4 +125,4 @@ def reset(self, event=None): # pylint: disable=unused-argument):):
"romversion", NA
)
self._lbl_gnss["text"] = self.__app.gnss_status.version_data.get("gnss", NA)
- self.__container.status_label = (HWRESET, OKCOL)
+ self.__container.set_status_label(HWRESET, OKCOL)
diff --git a/src/pygpsclient/helpers.py b/src/pygpsclient/helpers.py
index cc0e6bf6..cf82421d 100644
--- a/src/pygpsclient/helpers.py
+++ b/src/pygpsclient/helpers.py
@@ -97,6 +97,8 @@
VALNONBLANK,
VALNONSPACE,
VALREGEX,
+ VALSIGNED,
+ VALUNSIGNED,
VALURL,
Area,
Point,
@@ -134,7 +136,8 @@ def validate(
:param Entry self: tkinter entry widget instance
:param int valmode: int representing validation type - can be OR'd
- :param int | float low: optional min value
+ :param int | float low: optional min value, or length of field in \
+ bits for VALSIGNED/VALUNSIGNED
:param int | float high: optional max value
:param str | NoneType regex: regex expression
:param MethodType | FunctionType | NoneType func: custom validation function
@@ -154,11 +157,17 @@ def validate(
elif valmode == VALNONSPACE: # non-blank
valid = not val.isspace()
elif valmode == VALINT: # int in range
- valid = low < int(val) < high
+ valid = low <= int(val) <= high
elif valmode == VALBOOL: # boolean
valid = val in ("0", "1", 1, 0)
elif valmode == VALFLOAT: # float in range
- valid = low < float(val) < high
+ valid = low <= float(val) <= high
+ elif valmode == VALSIGNED: # signed integer (two's complement)
+ maxval = 2 ** (low - 1) # low = field length in bits
+ valid = -maxval <= int(val) <= maxval
+ elif valmode == VALUNSIGNED: # unsigned integer
+ maxval = (2**low) - 1 # low = field length in bits
+ valid = 0 <= int(val) <= maxval
elif valmode == VALURL: # valid URL
# none of the clever RFC 3986 regexes
# seem to work 100% of the time
@@ -196,7 +205,7 @@ def trace_update(
mode: Literal["array", "read", "write", "unset"],
callback: object,
add: bool = True,
-) -> str:
+) -> str | NoneType:
"""
Extends tkinter.*Var classes with trace_update method.
diff --git a/src/pygpsclient/importmap_dialog.py b/src/pygpsclient/importmap_dialog.py
index e57e27d9..6c6cce66 100644
--- a/src/pygpsclient/importmap_dialog.py
+++ b/src/pygpsclient/importmap_dialog.py
@@ -184,7 +184,7 @@ def _reset(self):
INFOCOL,
)
else:
- self.status_label = ""
+ self.set_status_label("")
def _valid_entries(self) -> bool:
"""
diff --git a/src/pygpsclient/menu_bar.py b/src/pygpsclient/menu_bar.py
index 8a39959f..b1e35b68 100644
--- a/src/pygpsclient/menu_bar.py
+++ b/src/pygpsclient/menu_bar.py
@@ -15,6 +15,7 @@
from tkinter import Menu
from pygpsclient.strings import (
+ DLGGUIOPTIONS,
DLGTABOUT,
DLGTGPX,
DLGTIMPORTMAP,
@@ -48,6 +49,7 @@
DLGTTTY,
DLGTRECORD,
DLGTRINEX,
+ DLGGUIOPTIONS,
)
diff --git a/src/pygpsclient/nmea_preset_frame.py b/src/pygpsclient/nmea_preset_frame.py
index ae1f5707..d2077a56 100644
--- a/src/pygpsclient/nmea_preset_frame.py
+++ b/src/pygpsclient/nmea_preset_frame.py
@@ -186,7 +186,7 @@ def _on_send_preset(self, *args, **kwargs): # pylint: disable=unused-argument
"""
if not self._ent_command.validate(VALREGEX, regex=NMEAPRESETREGEX):
- self.__container.status_label = ("Invalid command format", ERRCOL)
+ self.__container.set_status_label("Invalid command format", ERRCOL)
return
self._preset_command = self._command.get()
@@ -204,16 +204,16 @@ def _on_send_preset(self, *args, **kwargs): # pylint: disable=unused-argument
if status == CONFIRMED:
self._lbl_send_command["image"] = self._img_pending
- self.__container.status_label = "Command(s) sent"
+ self.__container.set_status_label("Command(s) sent")
for msgid in confids:
self.__container.set_pending(msgid, NMEA_PRESET)
elif status == CANCELLED:
- self.__container.status_label = "Command(s) cancelled"
+ self.__container.set_status_label("Command(s) cancelled")
elif status == NOMINAL:
- self.__container.status_label = "Command(s) sent, no results"
+ self.__container.set_status_label("Command(s) sent, no results")
except Exception as err: # pylint: disable=broad-except
- self.__container.status_label = (f"Error {err}", ERRCOL)
+ self.__container.set_status_label(f"Error {err}", ERRCOL)
self._lbl_send_command["image"] = self._img_warn
def _do_user_defined(self, command: str) -> list:
@@ -246,7 +246,7 @@ def _do_user_defined(self, command: str) -> list:
# self.logger.debug(f"{str(msg)=} - {msg.serialize()=} {confids=}")
self.__container.send_command(msg)
except Exception as err: # pylint: disable=broad-except
- self.__container.status_label = (f"Error {err}", ERRCOL)
+ self.__container.set_status_label(f"Error {err}", ERRCOL)
self._lbl_send_command["image"] = self._img_warn
return confids
@@ -261,7 +261,7 @@ def update_status(self, msg: NMEAMessage):
status = getattr(msg, "status", "OK")
if status == "OK":
self._lbl_send_command["image"] = self._img_confirmed
- self.__container.status_label = ("Preset command(s) acknowledged", OKCOL)
+ self.__container.set_status_label("Preset command(s) acknowledged", OKCOL)
elif status == "ERROR":
self._lbl_send_command["image"] = self._img_warn
- self.__container.status_label = ("Preset command(s) rejected", ERRCOL)
+ self.__container.set_status_label("Preset command(s) rejected", ERRCOL)
diff --git a/src/pygpsclient/ntrip_client_dialog.py b/src/pygpsclient/ntrip_client_dialog.py
index 4149b7a3..ce105fb2 100644
--- a/src/pygpsclient/ntrip_client_dialog.py
+++ b/src/pygpsclient/ntrip_client_dialog.py
@@ -418,6 +418,7 @@ def set_controls(self, connected: bool, msgt: NoneType | tuple = None):
self.__app.rtk_conn_status = CONNECTED_NTRIP if connected else DISCONNECTED
try:
+ msg = ""
self._settings = self.__app.ntrip_handler.settings
self._connected = connected
if msgt is None:
diff --git a/src/pygpsclient/resources/iconmonstr-antenna-3-24.png b/src/pygpsclient/resources/iconmonstr-antenna-3-24.png
deleted file mode 100644
index c2b160ad..00000000
Binary files a/src/pygpsclient/resources/iconmonstr-antenna-3-24.png and /dev/null differ
diff --git a/src/pygpsclient/resources/iconmonstr-gear-2-24.png b/src/pygpsclient/resources/iconmonstr-gear-2-24.png
new file mode 100644
index 00000000..294357c0
Binary files /dev/null and b/src/pygpsclient/resources/iconmonstr-gear-2-24.png differ
diff --git a/src/pygpsclient/resources/iconmonstr-save-disabled-24.png b/src/pygpsclient/resources/iconmonstr-save-disabled-24.png
new file mode 100644
index 00000000..2f553a71
Binary files /dev/null and b/src/pygpsclient/resources/iconmonstr-save-disabled-24.png differ
diff --git a/src/pygpsclient/rinex_dialog.py b/src/pygpsclient/rinex_dialog.py
index 71657a99..8e646ab2 100644
--- a/src/pygpsclient/rinex_dialog.py
+++ b/src/pygpsclient/rinex_dialog.py
@@ -941,7 +941,7 @@ def _do_conversion(self, **kwargs):
doi = self._doi.get()
licen = self._license.get()
station = self._station.get()
- comments = ("PyGPSClient RINEX Converter Dialog",)
+ comments = ("PyGPSClient RINEX Converter",)
country = self._countrycode.get()
for cvar in self._usercommentvar:
cval = cvar.get()
diff --git a/src/pygpsclient/serialconfig_frame.py b/src/pygpsclient/serialconfig_frame.py
index 188bab2f..e9fdd2d8 100644
--- a/src/pygpsclient/serialconfig_frame.py
+++ b/src/pygpsclient/serialconfig_frame.py
@@ -131,7 +131,7 @@ def __init__(self, app: Tk, parent: Frame, *args, **kwargs):
self._xonxoff = IntVar()
self._timeout = DoubleVar()
self._msgmode_name = StringVar()
- self._inactivity_timeout = IntVar()
+ self._inactivity_timeout = StringVar()
self._img_refresh = ImageTk.PhotoImage(Image.open(ICON_REFRESH))
self._img_expand = ImageTk.PhotoImage(Image.open(ICON_EXPAND))
self._img_contract = ImageTk.PhotoImage(Image.open(ICON_CONTRACT))
@@ -254,7 +254,7 @@ def _body(self):
self._lbl_inactivity = Label(self._frm_advanced, text="Inactivity Timeout (s)")
self._spn_inactivity = Spinbox(
self._frm_advanced,
- values=(0, 1, 5, 10, 30, 60, 300),
+ values=("0", "1", "5", "10", "30", "60", "300"),
width=4,
state=READONLY,
wrap=True,
@@ -347,7 +347,7 @@ def reset(self):
self._xonxoff.set(cfg.get("xonxoff_b"))
self._timeout.set(cfg.get("timeout_f"))
self._msgmode_name.set(MSGMODED[cfg.get("msgmode_n")])
- self._inactivity_timeout.set(cfg.get("inactivity_timeout_n"))
+ self._inactivity_timeout.set(int(cfg.get("inactivity_timeout_n")))
self.user_defined_port.set(cfg.get("userport_s"))
self._on_refresh_ports()
self._attach_events(True)
@@ -712,7 +712,7 @@ def inactivity_timeout(self) -> int:
:rtype: int
"""
- return self._inactivity_timeout.get()
+ return int(self._inactivity_timeout.get())
def _on_resize(self, event): # pylint: disable=unused-argument
"""
diff --git a/src/pygpsclient/settings_child_frame.py b/src/pygpsclient/settings_child_frame.py
index 7911f984..61437c30 100644
--- a/src/pygpsclient/settings_child_frame.py
+++ b/src/pygpsclient/settings_child_frame.py
@@ -110,7 +110,7 @@
)
MAXLINES = ("200", "500", "1000", "2000", "100")
-FILEDELAYS = (5, 10, 20, 50, 100, 200, 500, 1000, 2000) # ms
+FILEDELAYS = ("5", "10", "20", "50", "100", "200", "500", "1000", "2000") # ms
class SettingsChildFrame(Frame):
@@ -342,7 +342,7 @@ def _body(self):
)
self._spn_filedelay = Spinbox(
self._frm_options,
- value=FILEDELAYS,
+ values=FILEDELAYS,
width=4,
wrap=True,
textvariable=self._filedelay,
@@ -357,7 +357,7 @@ def _body(self):
)
self._spn_datalog = Spinbox(
self._frm_options,
- values=(FORMATS),
+ values=FORMATS,
width=20,
wrap=True,
textvariable=self._logformat,
@@ -692,14 +692,14 @@ def _on_update_maxlines(self, var, index, mode):
Action on updating console maxlines.
"""
- self.__app.configuration.set("maxlines_n", self._maxlines.get())
+ self.__app.configuration.set("maxlines_n", int(self._maxlines.get()))
def _on_update_filedelay(self, var, index, mode):
"""
Action on updating filedelay.
"""
- self.__app.configuration.set("filedelay_n", self._filedelay.get())
+ self.__app.configuration.set("filedelay_n", int(self._filedelay.get()))
def _on_update_degreesformat(self, var, index, mode):
"""
diff --git a/src/pygpsclient/strings.py b/src/pygpsclient/strings.py
index 6c605ac0..36be309f 100644
--- a/src/pygpsclient/strings.py
+++ b/src/pygpsclient/strings.py
@@ -15,7 +15,7 @@
from pygpsclient.globals import GITHUB_URL, MAPAPI_URL
TITLE = "PyGPSClient"
-COPYRIGHT = "\u00a9 2020-2026 semuadmin (Steve Smith)\nBSD 3-Clause License"
+COPYRIGHT = "\u00a9 2020-2026 semuadmin (Steve Smith) BSD 3-Clause License"
EPILOG = f"{COPYRIGHT} - {GITHUB_URL}"
INTROTXT = f"Welcome to {TITLE}!"
@@ -23,12 +23,11 @@
HELPTXT = "Help..About - display About dialog."
-ABOUTTXT = [
- f"{TITLE} is a free, open-source GNSS diagnostic and configuration",
- "application written entirely by volunteers in Python and tkinter. It",
- "supports NMEA, UBX, SBF, UNI, QGC, RTCM3, NTRIP & SPARTN protocols.",
- "Instructions and source code are available on GitHub at the link below.",
-]
+ABOUTTXT = (
+ f"{TITLE} is a free, open-source GNSS diagnostic and configuration "
+ "application written entirely by human volunteers in Python and tkinter. "
+ "It supports NMEA, UBX, SBF, UNI, QGC, RTCM3, NTRIP & SPARTN protocols."
+)
NA = "N/A"
# Message text
@@ -77,6 +76,7 @@
RINEXFILEVALID = "{path} validated; {count:,} records"
RINEXFILEVALIDATING = "Validating {path} ..."
RINEXOUTPUT = "Data converted, *.rnx output(s) in {parent}"
+ROMVERWARN = "Warning - this device may not support {generation} UBX configuration"
SAVECONFIGBAD = "Configuration not saved {}"
SAVECONFIGOK = "Configuration saved OK"
SAVEERROR = "ERROR! File could not be saved to specified directory"
@@ -215,6 +215,7 @@
DLGGPXOOB = "Map out of bounds\nTry increasing zoom level"
DLGGPXOPEN = "Click folder icon to open GPX Track file"
DLGGPXWAIT = "Redrawing..."
+DLGGUIOPTIONS = "GUI Configuration"
DLGHOWTO = f"How To Use {TITLE}"
DLGJSONERR = "Error! {}"
DLGJSONOK = "Keys loaded from {}"
diff --git a/src/pygpsclient/ubx_cfgval_frame.py b/src/pygpsclient/ubx_cfgval_frame.py
index 1e90bb1e..a80036f4 100644
--- a/src/pygpsclient/ubx_cfgval_frame.py
+++ b/src/pygpsclient/ubx_cfgval_frame.py
@@ -56,8 +56,9 @@
VALBOOL,
VALCUSTOM,
VALFLOAT,
- VALINT,
VALNONBLANK,
+ VALSIGNED,
+ VALUNSIGNED,
)
from pygpsclient.helpers import valid_hex
@@ -340,7 +341,7 @@ def _do_valset(self):
atts = attsiz(self._cfgatt.get())
except ValueError as err:
self._ent_val.validate(VALNONBLANK)
- self.__container.status_label = (f"INVALID ENTRY - {err}", ERRCOL)
+ self.__container.set_status_label(f"INVALID ENTRY - {err}", ERRCOL)
return False
val = self._cfgval.get()
layers = self._cfglayer.get()
@@ -362,22 +363,21 @@ def _do_valset(self):
if valid:
val = int.to_bytes(int(val, 16), atts, "little")
elif att in ("E", "U"): # unsigned integer
- self._ent_val.validate(VALINT)
- val = int(val)
- if val < 0:
- valid = False
-
- elif att == "L": # bool
- self._ent_val.validate(VALBOOL)
- val = int(val)
- if val not in (0, 1):
- valid = False
+ valid = self._ent_val.validate(VALUNSIGNED, atts * 8)
+ if valid:
+ val = int(val)
elif att == "I": # signed integer
- self._ent_val.validate(VALINT)
- val = int(val)
+ valid = self._ent_val.validate(VALSIGNED, atts * 8)
+ if valid:
+ val = int(val)
+ elif att == "L": # bool
+ valid = self._ent_val.validate(VALBOOL)
+ if valid:
+ val = int(val)
elif att == "R": # floating point
- self._ent_val.validate(VALFLOAT)
- val = float(val)
+ valid = self._ent_val.validate(VALFLOAT)
+ if valid:
+ val = float(val)
transaction = 0
cfgData = [
(self._cfgval_keyname, val),
@@ -389,13 +389,13 @@ def _do_valset(self):
msg = UBXMessage.config_set(layers, transaction, cfgData)
self.__container.send_command(msg)
self._lbl_send_command["image"] = self._img_pending
- self.__container.status_label = "CFG-VALSET SET message sent"
+ self.__container.set_status_label("CFG-VALSET SET message sent")
for msgid in ("ACK-ACK", "ACK-NAK"):
self.__container.set_pending(msgid, UBX_CFGVAL)
else:
- self._lbl_send_command["image"] = self._img_warn
+ # self._lbl_send_command["image"] = self._img_warn
typ = ATTDICT[att]
- self.__container.status_label = (
+ self.__container.set_status_label(
(
"INVALID ENTRY - must conform to parameter "
f"type {att} ({typ}) and size {atts} bytes"
@@ -424,7 +424,7 @@ def _do_valdel(self):
msg = UBXMessage.config_del(layers, transaction, key)
self.__container.send_command(msg)
self._lbl_send_command["image"] = self._img_pending
- self.__container.status_label = "CFG-VALDEL SET message sent"
+ self.__container.set_status_label("CFG-VALDEL SET message sent")
for msgid in ("ACK-ACK", "ACK-NAK"):
self.__container.set_pending(msgid, UBX_CFGVAL)
@@ -449,7 +449,7 @@ def _do_valget(self):
msg = UBXMessage.config_poll(layers, transaction, keys)
self.__container.send_command(msg)
self._lbl_send_command["image"] = self._img_pending
- self.__container.status_label = "CFG-VALGET POLL message sent"
+ self.__container.set_status_label("CFG-VALGET POLL message sent")
for msgid in ("CFG-VALGET", "ACK-ACK", "ACK-NAK"):
self.__container.set_pending(msgid, UBX_CFGVAL)
@@ -469,12 +469,12 @@ def update_status(self, msg: UBXMessage): # pylint: disable=unused-argument
vali = int.from_bytes(val, "little")
val = f"0x{vali:0{atts*2}x}"
self._cfgval.set(val)
- self.__container.status_label = ("CFG-VALGET GET message received", OKCOL)
+ self.__container.set_status_label("CFG-VALGET GET message received", OKCOL)
elif msg.identity == "ACK-ACK":
self._lbl_send_command["image"] = self._img_confirmed
- self.__container.status_label = ("CFG-VAL command acknowledged", OKCOL)
+ self.__container.set_status_label("CFG-VAL command acknowledged", OKCOL)
elif msg.identity == "ACK-NAK":
self._lbl_send_command["image"] = self._img_warn
- self.__container.status_label = ("CFG-VAL command rejected", ERRCOL)
+ self.__container.set_status_label("CFG-VAL command rejected", ERRCOL)
diff --git a/src/pygpsclient/ubx_config_dialog.py b/src/pygpsclient/ubx_config_dialog.py
index 15b3bb8d..7e25d419 100644
--- a/src/pygpsclient/ubx_config_dialog.py
+++ b/src/pygpsclient/ubx_config_dialog.py
@@ -32,6 +32,7 @@
CONNECTED_SIMULATOR,
CONNECTED_SOCKET,
ERRCOL,
+ ROMVER_NEW,
UBX_CFGVAL,
UBX_MONHW,
UBX_MONRF,
@@ -39,7 +40,7 @@
UBX_PRESET,
)
from pygpsclient.hardware_info_frame import Hardware_Info_Frame
-from pygpsclient.strings import DLGTUBX
+from pygpsclient.strings import DLGTUBX, NA, NOTCONN, ROMVERWARN
from pygpsclient.toplevel_dialog import ToplevelDialog
from pygpsclient.ubx_cfgval_frame import UBX_CFGVAL_Frame
from pygpsclient.ubx_preset_frame import UBX_PRESET_Frame
@@ -115,13 +116,21 @@ def _reset(self):
Reset configuration widgets.
"""
- self.frm_device_info.reset()
if self.__app.conn_status not in (
CONNECTED,
CONNECTED_SOCKET,
CONNECTED_SIMULATOR,
):
- self.set_status_label("Device not connected", ERRCOL)
+ self.set_status_label(NOTCONN, ERRCOL)
+ return
+
+ # check for modern ROM version
+ hwver = self.__app.gnss_status.version_data["hwversion"]
+ romver = self.__app.gnss_status.version_data["romversion"]
+ if "u-blox" not in hwver or (romver < ROMVER_NEW and romver != NA):
+ self.set_status_label(ROMVERWARN.format(generation="modern"), ERRCOL)
+ else:
+ self.frm_device_info.reset()
def _attach_events(self):
"""
diff --git a/src/pygpsclient/ubx_legacy_config_dialog.py b/src/pygpsclient/ubx_legacy_config_dialog.py
index bbba05f6..2c72f329 100644
--- a/src/pygpsclient/ubx_legacy_config_dialog.py
+++ b/src/pygpsclient/ubx_legacy_config_dialog.py
@@ -33,20 +33,19 @@
CONNECTED_SIMULATOR,
CONNECTED_SOCKET,
ERRCOL,
+ ROMVER_NEW,
UBX_CFGMSG,
UBX_CFGOTHER,
UBX_CFGPRT,
- UBX_CFGRATE,
UBX_MONHW,
UBX_MONRF,
UBX_MONVER,
)
from pygpsclient.hardware_info_frame import Hardware_Info_Frame
-from pygpsclient.strings import DLGTUBXLEGACY
+from pygpsclient.strings import DLGTUBXLEGACY, NA, NOTCONN, ROMVERWARN
from pygpsclient.toplevel_dialog import ToplevelDialog
from pygpsclient.ubx_msgrate_frame import UBX_MSGRATE_Frame
from pygpsclient.ubx_port_frame import UBX_PORT_Frame
-from pygpsclient.ubx_solrate_frame import UBX_RATE_Frame
class UBXLegacyConfigDialog(ToplevelDialog):
@@ -88,9 +87,6 @@ def _body(self):
self._frm_config_port = UBX_PORT_Frame(
self.__app, self, borderwidth=2, relief="groove"
)
- self._frm_config_rate = UBX_RATE_Frame(
- self.__app, self, borderwidth=2, relief="groove"
- )
self._frm_config_msg = UBX_MSGRATE_Frame(
self.__app, self, borderwidth=2, relief="groove"
)
@@ -105,14 +101,11 @@ def _do_layout(self):
self.frm_device_info.grid(column=0, row=0, columnspan=3, sticky=NSEW)
self._frm_config_port.grid(column=0, row=1, sticky=NSEW)
- self._frm_config_rate.grid(column=0, row=2, sticky=NSEW)
- self._frm_config_msg.grid(column=0, row=3, sticky=NSEW)
- self._frm_config_dynamic.grid(column=1, row=1, rowspan=3, sticky=NSEW)
-
- for row in range(0, 2):
- self.container.grid_columnconfigure(row, weight=1)
- for row in range(1, 4):
- self.container.grid_rowconfigure(row, weight=1)
+ self._frm_config_msg.grid(column=0, row=2, sticky=NSEW)
+ self._frm_config_dynamic.grid(column=1, row=1, rowspan=2, sticky=NSEW)
+
+ self.container.grid_columnconfigure(1, weight=1)
+ self.container.grid_rowconfigure(2, weight=1)
self._frm_config_msg.grid_rowconfigure(3, weight=1)
self._frm_config_dynamic.grid_columnconfigure(0, weight=1)
self._frm_config_dynamic.grid_rowconfigure(1, weight=1)
@@ -122,16 +115,23 @@ def _reset(self):
Reset configuration widgets.
"""
- self._frm_config_rate.reset()
- self._frm_config_port.reset()
- self._frm_config_dynamic.reset()
- self.frm_device_info.reset()
if self.__app.conn_status not in (
CONNECTED,
CONNECTED_SOCKET,
CONNECTED_SIMULATOR,
):
- self.set_status_label("Device not connected", ERRCOL)
+ self.set_status_label(NOTCONN, ERRCOL)
+ return
+
+ # check for legacy ROM version
+ hwver = self.__app.gnss_status.version_data["hwversion"]
+ romver = self.__app.gnss_status.version_data["romversion"]
+ if "u-blox" not in hwver or (romver >= ROMVER_NEW and romver != NA):
+ self.set_status_label(ROMVERWARN.format(generation="legacy"), ERRCOL)
+ else:
+ self._frm_config_port.reset()
+ self._frm_config_dynamic.reset()
+ self.frm_device_info.reset()
def _attach_events(self):
"""
@@ -166,8 +166,6 @@ def update_pending(self, msg: UBXMessage):
self.frm_device_info.reset()
elif ubxfrm == UBX_CFGPRT:
self._frm_config_port.update_status(msg)
- elif ubxfrm == UBX_CFGRATE:
- self._frm_config_rate.update_status(msg)
elif ubxfrm == UBX_CFGMSG:
self._frm_config_msg.update_status(msg)
elif ubxfrm == UBX_CFGOTHER:
diff --git a/src/pygpsclient/ubx_msgrate_frame.py b/src/pygpsclient/ubx_msgrate_frame.py
index dba17987..2992f829 100644
--- a/src/pygpsclient/ubx_msgrate_frame.py
+++ b/src/pygpsclient/ubx_msgrate_frame.py
@@ -196,7 +196,7 @@ def update_status(self, msg: UBXMessage):
"""
if msg.identity == "CFG-MSG":
- self.__container.status_label = ("CFG-MSG GET message received", OKCOL)
+ self.__container.set_status_label("CFG-MSG GET message received", OKCOL)
self._ddc_rate.set(msg.rateDDC)
self._uart1_rate.set(msg.rateUART1)
self._uart2_rate.set(msg.rateUART2)
@@ -205,7 +205,7 @@ def update_status(self, msg: UBXMessage):
self._lbl_send_command["image"] = self.__container.img_confirmed
elif msg.identity == "ACK-NAK":
- self.__container.status_label = ("CFG-MSG POLL message rejected", ERRCOL)
+ self.__container.set_status_label("CFG-MSG POLL message rejected", ERRCOL)
self._lbl_send_command["image"] = self.__container.img_warn
def _on_select_cfg_msg(self, *args, **kwargs): # pylint: disable=unused-argument
@@ -247,7 +247,7 @@ def _on_send_cfg_msg(self, *args, **kwargs): # pylint: disable=unused-argument
)
self.__container.send_command(msg)
self._lbl_send_command["image"] = self.__container.img_pending
- self.__container.status_label = "CFG-MSG SET message sent"
+ self.__container.set_status_label("CFG-MSG SET message sent")
for msgid in ("ACK-ACK", "ACK-NAK"):
self.__container.set_pending(msgid, UBX_CFGMSG)
@@ -263,6 +263,6 @@ def _do_poll_msg(self, msgtyp: bytes):
msg = UBXMessage("CFG", "CFG-MSG", POLL, payload=msgtyp)
self.__container.send_command(msg)
self._lbl_send_command["image"] = self.__container.img_pending
- self.__container.status_label = "CFG-MSG POLL message sent"
+ self.__container.set_status_label("CFG-MSG POLL message sent")
for msgid in ("CFG-MSG", "ACK-NAK"):
self.__container.set_pending(msgid, UBX_CFGMSG)
diff --git a/src/pygpsclient/ubx_port_frame.py b/src/pygpsclient/ubx_port_frame.py
index dc8b2c34..6d697bd9 100644
--- a/src/pygpsclient/ubx_port_frame.py
+++ b/src/pygpsclient/ubx_port_frame.py
@@ -189,10 +189,10 @@ def update_status(self, msg: UBXMessage):
self._outprot_nmea.set(msg.outNMEA)
self._outprot_rtcm3.set(msg.outRTCM3)
self._lbl_send_command["image"] = self.__container.img_confirmed
- self.__container.status_label = ("CFG-PRT GET message received", OKCOL)
+ self.__container.set_status_label("CFG-PRT GET message received", OKCOL)
elif msg.identity == "ACK-NAK":
- self.__container.status_label = ("CFG-PRT POLL message rejected", ERRCOL)
+ self.__container.set_status_label("CFG-PRT POLL message rejected", ERRCOL)
self._lbl_send_command["image"] = self.__container.img_warn
def _on_select_portid(self):
@@ -235,7 +235,7 @@ def _on_send_port(self, *args, **kwargs): # pylint: disable=unused-argument
)
self.__container.send_command(msg)
self._lbl_send_command["image"] = self.__container.img_pending
- self.__container.status_label = "CFG-PRT SET message sent"
+ self.__container.set_status_label("CFG-PRT SET message sent")
for msgid in ("ACK-NAK", "ACK-NAK"):
self.__container.set_pending(msgid, UBX_CFGPRT)
@@ -250,6 +250,6 @@ def _do_poll_prt(self, *args, **kwargs): # pylint: disable=unused-argument
msg = UBXMessage("CFG", "CFG-PRT", POLL, portID=portID)
self.__container.send_command(msg)
self._lbl_send_command["image"] = self.__container.img_pending
- self.__container.status_label = "CFG-PRT POLL message sent"
+ self.__container.set_status_label("CFG-PRT POLL message sent")
for msgid in ("CFG-PRT", "ACK-NAK"):
self.__container.set_pending(msgid, UBX_CFGPRT)
diff --git a/src/pygpsclient/ubx_preset_frame.py b/src/pygpsclient/ubx_preset_frame.py
index 29ca35dc..b97398bb 100644
--- a/src/pygpsclient/ubx_preset_frame.py
+++ b/src/pygpsclient/ubx_preset_frame.py
@@ -188,16 +188,16 @@ def _on_send_preset(self, *args, **kwargs): # pylint: disable=unused-argument
if status == CONFIRMED:
self._lbl_send_command["image"] = self.__container.img_pending
- self.__container.status_label = "Command(s) sent"
+ self.__container.set_status_label("Command(s) sent")
for msgid in confids:
self.__container.set_pending(msgid, UBX_PRESET)
elif status == CANCELLED:
- self.__container.status_label = "Command(s) cancelled"
+ self.__container.set_status_label("Command(s) cancelled")
elif status == NOMINAL:
- self.__container.status_label = "Command(s) sent, no results"
+ self.__container.set_status_label("Command(s) sent, no results")
except Exception as err: # pylint: disable=broad-except
- self.__container.status_label = (f"Error {err}", ERRCOL)
+ self.__container.set_status_label(f"Error {err}", ERRCOL)
self._lbl_send_command["image"] = self.__container.img_warn
def _format_preset(self, command: str):
@@ -225,7 +225,7 @@ def _format_preset(self, command: str):
msg = UBXMessage(ubx_class, ubx_id, mode)
self.__container.send_command(msg)
except Exception as err: # pylint: disable=broad-except
- self.__container.status_label = (f"Error {err}", ERRCOL)
+ self.__container.set_status_label(f"Error {err}", ERRCOL)
self._lbl_send_command["image"] = self.__container.img_warn
def update_status(self, msg: UBXMessage):
@@ -237,7 +237,7 @@ def update_status(self, msg: UBXMessage):
if msg.identity in ("ACK-ACK", "MON-VER"):
self._lbl_send_command["image"] = self.__container.img_confirmed
- self.__container.status_label = ("Preset command(s) acknowledged", OKCOL)
+ self.__container.set_status_label("Preset command(s) acknowledged", OKCOL)
elif msg.identity == "ACK-NAK":
self._lbl_send_command["image"] = self.__container.img_warn
- self.__container.status_label = ("Preset command(s) rejected", ERRCOL)
+ self.__container.set_status_label("Preset command(s) rejected", ERRCOL)
diff --git a/src/pygpsclient/ubx_solrate_frame.py b/src/pygpsclient/ubx_solrate_frame.py
deleted file mode 100644
index 6d8ea232..00000000
--- a/src/pygpsclient/ubx_solrate_frame.py
+++ /dev/null
@@ -1,196 +0,0 @@
-"""
-ubx_solrate_frame.py
-
-UBX Configuration frame for CFG-RATE commands.
-
-Created on 19 Feb 2020
-
-:author: semuadmin (Steve Smith)
-:copyright: 2020 semuadmin
-:license: BSD 3-Clause
-"""
-
-from tkinter import EW, Button, E, Frame, IntVar, Label, Spinbox, StringVar, Tk, W
-
-from pyubx2 import POLL, SET, UBXMessage
-
-from pygpsclient.globals import (
- CLICK_CURSOR,
- CONNECTED,
- ERRCOL,
- OKCOL,
- READONLY,
- UBX_CFGRATE,
-)
-from pygpsclient.strings import LBLCFGRATE
-
-TIMEREFS = {
- 0: "UTC",
- 1: "GPS",
- 2: "GLO",
- 3: "BEI",
- 4: "GAL",
-}
-
-
-class UBX_RATE_Frame(Frame):
- """
- UBX Navigation Solution Rate configuration command panel.
- """
-
- def __init__(self, app: Tk, parent: Frame, *args, **kwargs):
- """
- Constructor.
-
- :param Tk app: reference to main tkinter application
- :param Frame parent: reference to parent frame (config-dialog)
- :param args: optional args to pass to Frame parent class
- :param kwargs: optional kwargs to pass to Frame parent class
- """
-
- self.__app = app # Reference to main application class
- self.__container = parent
-
- super().__init__(parent.container, *args, **kwargs)
-
- self._measint = IntVar()
- self._navrate = IntVar()
- self._timeref = StringVar()
-
- self._body()
- self._do_layout()
- self._attach_events()
-
- def _body(self):
- """
- Set up frame and widgets.
- """
-
- self._lbl_cfg_rate = Label(self, text=LBLCFGRATE, anchor=W)
- self._lbl_ubx_measint = Label(self, text="Solution Interval (ms)")
- self._spn_ubx_measint = Spinbox(
- self,
- values=(25, 50, 100, 200, 250, 500, 1000, 2000, 5000, 10000),
- width=6,
- state=READONLY,
- wrap=True,
- textvariable=self._measint,
- )
- self._lbl_ubx_navrate = Label(self, text="Measurement Ratio")
- self._spn_ubx_navrate = Spinbox(
- self,
- from_=1,
- to=127,
- width=4,
- state=READONLY,
- wrap=True,
- textvariable=self._navrate,
- )
- self._lbl_ubx_timeref = Label(self, text="Time Reference")
- self._spn_ubx_timeref = Spinbox(
- self,
- values=list(TIMEREFS.values()),
- width=5,
- state=READONLY,
- wrap=True,
- textvariable=self._timeref,
- )
- self._lbl_send_command = Label(self, image=self.__container.img_none)
- self._btn_send_command = Button(
- self,
- image=self.__container.img_send,
- width=50,
- command=self._on_send_rate,
- cursor=CLICK_CURSOR,
- )
-
- def _do_layout(self):
- """
- Layout widgets.
- """
-
- self._lbl_cfg_rate.grid(column=0, row=0, columnspan=6, sticky=EW)
- self._lbl_ubx_measint.grid(column=0, row=1, columnspan=2, sticky=W)
- self._spn_ubx_measint.grid(column=2, row=1, sticky=W)
- self._lbl_ubx_navrate.grid(column=0, row=2, columnspan=2, sticky=W)
- self._spn_ubx_navrate.grid(column=2, row=2, columnspan=2, sticky=W)
- self._lbl_ubx_timeref.grid(column=0, row=3, columnspan=2, sticky=W)
- self._spn_ubx_timeref.grid(column=2, row=3, columnspan=2, sticky=W)
- self._btn_send_command.grid(
- column=4, row=1, rowspan=3, ipadx=3, ipady=3, sticky=E
- )
- self._lbl_send_command.grid(
- column=5, row=1, rowspan=3, ipadx=3, ipady=3, sticky=E
- )
-
- def _attach_events(self):
- """
- Bind events to widget.
- """
-
- # click mouse button to refresh information
- self.bind("", self._do_poll_rate)
-
- def reset(self):
- """
- Reset panel to initial settings.
- """
-
- if self.__app.conn_status == CONNECTED:
- self._do_poll_rate()
-
- def update_status(self, msg: UBXMessage):
- """
- Update pending confirmation status.
-
- :param UBXMessage msg: UBX config message
- """
-
- if msg.identity == "CFG-RATE":
- self._measint.set(msg.measRate)
- self._navrate.set(msg.navRate)
- self._timeref.set(TIMEREFS[msg.timeRef])
- self._lbl_send_command["image"] = self.__container.img_confirmed
- self.__container.status_label = ("CFG-RATE GET message received", OKCOL)
-
- elif msg.identity == "ACK-NAK":
- self.__container.status_label = ("CFG-RATE POLL message rejected", ERRCOL)
- self._lbl_send_command["image"] = self.__container.img_warn
-
- def _on_send_rate(self, *args, **kwargs): # pylint: disable=unused-argument
- """
- Handle Send rate config button press.
- """
-
- tref = 0
- for key, val in TIMEREFS.items():
- if val == self._timeref.get():
- tref = key
- break
-
- msg = UBXMessage(
- "CFG",
- "CFG-RATE",
- SET,
- measRate=self._measint.get(),
- navRate=self._navrate.get(),
- timeRef=tref,
- )
- self.__container.send_command(msg)
- self._lbl_send_command["image"] = self.__container.img_pending
- self.__container.status_label = "CFG-RATE SET message sent"
- self.__container.set_pending(UBX_CFGRATE, ("ACK-ACK", "ACK-NAK"))
-
- self._do_poll_rate()
-
- def _do_poll_rate(self, *args, **kwargs): # pylint: disable=unused-argument
- """
- Poll RATE message configuration.
- """
-
- msg = UBXMessage("CFG", "CFG-RATE", POLL)
- self.__container.send_command(msg)
- self._lbl_send_command["image"] = self.__container.img_pending
- self.__container.status_label = "CFG-RATE POLL message sent"
- for msgid in ("CFG-RATE", "ACK-NAK"):
- self.__container.set_pending(msgid, UBX_CFGRATE)