diff --git a/README.md b/README.md
index 0c33f4ec..41c5e5de 100644
--- a/README.md
+++ b/README.md
@@ -120,7 +120,6 @@ For more comprehensive installation instructions, please refer to [INSTALLATION.
1. File Delay - Select delay in milliseconds between individual reads when streaming from binary file (default 20 milliseconds).
1. Tags - Enable color tags in console (see Console Widget below).
1. Position Format and Units - Change the displayed position (D.DD / D.M.S / D.M.MM / ECEF) and unit (metric/imperial) formats.
-1. Include C/No = 0 - Include or exclude satellites where carrier to noise ratio (C/No) = 0.
1. DataLogging - Turn Data logging in the selected format (Binary, Parsed, Hex Tabular, Hex String, Parsed+Hex Tabular) on or off. On first selection, you will be prompted to select the directory into which timestamped log files are saved. Log files are cycled when a maximum size is reached (default is 10 MB, manually configurable via `logsize_n` setting).
1. GPX Track - Turn track recording (in GPX format) on or off. On first selection, you will be prompted to select the directory into which timestamped GPX track files are saved.
1. Database - Turn spatialite database recording (*where available*) on or off. On first selection, you will be prompted to select the directory into which the `pygpsclient.sqlite` database is saved. Note that, when first created, the database's spatial metadata will take a few seconds to initialise (*up to a minute on Raspberry Pi and similar SBC*). **NB** This facility is dependent on your Python environment supporting the requisite [sqlite3 `mod_spatialite` extension](https://www.gaia-gis.it/fossil/libspatialite/index) - see [INSTALLATION.md](https://github.com/semuconsulting/PyGPSClient/blob/master/INSTALLATION.md#prereqs) for further details. If not supported, the option will be greyed out. Check the Menu..Help..About dialog for an indication of the current spatialite support status.
@@ -129,7 +128,6 @@ For more comprehensive installation instructions, please refer to [INSTALLATION.
1. To save the current configuration to a file, go to File..Save Configuration.
1. To load a saved configuration file, go to File..Load Configuration. The default configuration file location is `$HOME/pygpsclient.json`.
**NB** Any active serial or RTK connection must be stopped before loading a new configuration.
-1. [Socket Server / NTRIP Caster](#socketserver) facility with two modes of operation: (a) open, unauthenticated Socket Server or (b) NTRIP Caster (mountpoint = `pygnssutils`).
1. [UBX Configuration Dialog](#ubxconfig), with the ability to send a variety of UBX CFG configuration commands to u-blox GNSS devices. This includes the facility to add **user-defined commands or command sequences** - see instructions under [user-defined presets](#userdefined) below. To display the UBX Configuration Dialog (*only functional when connected to a UBX GNSS device via serial port*), click
, or go to Menu..Options..UBX Configuration Dialog.
1. [NMEA Configuration Dialog](#nmeaconfig), with the ability to send a variety of NMEA configuration commands to GNSS devices (e.g. Quectel LG290P). This includes the facility to add **user-defined commands or command sequences** - see instructions under [user-defined presets](#userdefined) below. To display the NMEA Configuration Dialog (*only functional when connected to a compatible GNSS device via serial port*), click , or go to Menu..Options..NMEA Configuration Dialog.
@@ -137,6 +135,8 @@ For more comprehensive installation instructions, please refer to [INSTALLATION.
, or go to Menu..Options..TTY Commands.
1. [NTRIP Client](#ntripconfig) facility with the ability to connect to a specified NTRIP caster, parse the incoming RTCM3 or SPARTN data and feed this data to a compatible GNSS receiver (*requires an Internet connection and access to an NTRIP caster and local mountpoint*). To display the NTRIP Client Configuration Dialog, click
, or go to Menu..Options..NTRIP Configuration Dialog.
+1. [Server Config](#socketserver) facility with the ability to act as generic socket server or NTRIP caster (mountpoint = `pygnssutils`). To display the Server Configuration Dialog, click
+, or go to Menu..Options..Server Configuration Dialog.
1. [SPARTN Client](#spartnconfig) facility with the ability to configure an IP or L-Band SPARTN Correction source and SPARTN-compatible GNSS receiver (e.g. ZED-F9P) and pass the incoming correction data to the GNSS receiver (*requires an Internet connection and access to a SPARTN location service*). To display the SPARTN Client Configuration Dialog, go to Menu..Options..SPARTN Configuration Dialog.
1. [GPX Track Viewer](#gpxviewer) utility with elevation and speed profiles and track metadata. To display the GPX Track viewer, go to Menu..Options..GPX Track Viewer.
@@ -162,7 +162,8 @@ For more comprehensive installation instructions, please refer to [INSTALLATION.
|| Expandable banner showing key navigation status information based on messages received from receiver. To expand or collapse the banner or serial port configuration widgets, click the / buttons. **NB**: some fields (e.g. hdop/vdop, hacc/vacc) are only available from proprietary NMEA or UBX messages and may not be output by default. The minimum messages required to populate all available fields are: NMEA: GGA, GSA, GSV, RMC, UBX00 (proprietary); UBX: NAV-DOP, NAV-PVT, NAV-SAT |
|| Configurable serial console widget showing incoming GNSS data streams in either parsed, binary or tabular hexadecimal formats. Double-right-click to copy contents of console to the clipboard. The scroll behaviour and number of lines retained in the console can be configured via the settings panel. Supports user-configurable color tagging of selected strings for easy identification. Color tags are loaded from the `"colortag_b":` value (`0` = disable, `1` = enable) and `"colortags_l":` list (`[string, color]` pairs) in your json configuration file (see example provided). If color is set to "HALT", streaming will halt on any match and a warning displayed. NB: color tagging does impose a small performance overhead - turning it off will improve console response times at very high transaction rates.|
|| Skyview widget showing current satellite visibility and position (elevation / azimuth). Satellite icon borders are colour-coded to distinguish between different GNSS constellations. For consistency between NMEA and UBX data sources, will display GLONASS NMEA SVID (65-96) rather than slot (1-24). |
-|| Levels view widget showing current satellite carrier-to-noise (C/No) levels for each GNSS constellation. Double-click to toggle legend. |
+|| Levels view widget showing current satellite carrier-to-noise (C/No) levels for each GNSS constellation. Double-click to toggle legend. Double-right-click to toggle levels where C/No = 0 dbHz. |
+|| Signals view widget showing current svid/signal carrier-to-noise (C/No) level and (where applicable) correction source for each GNSS svid/signal received (*GNSS receiver must be capable of outputting UBX NAV-SIG messages*). Signal identifiers are in RINEX format e.g. `L1_C/A`, `E5_aQ`, etc. Double-click to toggle legend. Double-right-click to toggle signals where C/No = 0 dbHz. |
|| Map widget with various modes of display - select from "map" / "sat" (online) or "world" / "custom" (offline). Select zoom level 1 - 20. Double-click the zoom level label to reset the zoom to 10. Double-right-click the zoom label to maximise zoom to 20. Tick Track to show track (track will only be recorded while this box is checked). Double-Right-click will clear the map. Map Type = 'world': a static offline Mercator world map showing current global location.
|| Map Type = 'map', 'sat' or 'hyb' (hybrid): Dynamic, online web map or satellite image via MapQuest API (*requires an Internet connection and free [Mapquest API Key](#mapquestapi)*). By default, the web map will automatically refresh every 60 seconds (*indicated by a small timer icon at the top left*). The default refresh rate can be amended by changing the `"mapupdateinterval_n":` value in your json configuration file, but **NB** the facility is not intended to be used for real-time navigation. Double-click anywhere in the map to immediately refresh. |
|| Map Type = 'custom': One or more user-defined offline geo-referenced map images can be imported using the Menu..Options..Import Custom Map facility, or by manually setting the `usermaps_l` field in the json configuration file. The `usermaps_l` setting represents a list of map paths and extents in the format ["path to map image", [minlat, minlon, maxlat, maxlon]] - see [example configuration file](https://github.com/semuconsulting/PyGPSClient/blob/master/pygpsclient.json#L281). Map images must be a [supported format](https://pillow.readthedocs.io/en/stable/handbook/image-file-formats.html) and use a standard WGS84 Web Mercator projection e.g. EPSG:4326. PyGPSClient will automatically select the first map whose extents encompass the current location, based on the order in which the maps appear in `usermaps_l`. NB: The minimum and maximum viable 'zoom' levels depend on the resolution and extents of the imported image and the user's display - if the zoom bounds exceed the image extents, the Zoom spinbox will be highlighted. Offline and online zoom levels will not necessarily correspond. |
@@ -257,7 +258,14 @@ The following example illustrates a series of ASCII configuration commands being

-This allows users to record  a sequence of UBX, NMEA or TTY configuration commands as they are sent to a device, and to save  this recording to a file. Saved files can be reloaded  and the configuration commands replayed . This provides a means to easily reproduce a given sequence of configuration commands, or copy a saved configuration between compatible devices. The Configuration Load facility can accept configuration files in either UBX/NMEA binary (\*.bin), TTY (\*.tty) or u-center UBX text format (\*.txt). Files saved using the [ubxsave](#ubxsave) CLI utility (*installed via the `pygnssutils` library*) can also be reloaded and replayed. **Tip:** The contents of a binary (\*.bin) config file can be reviewed using PyGPSClient's [file streaming facility](#filestream), *BUT* remember to set the `Msg Mode` in the Settings panel to `SET` rather than the default `GET` .
+The Configuration Command Load/Save/Record facility supports the following functionality:
+1. It allows users to record  a sequence of UBX, NMEA or TTY configuration commands as they are sent to a device, and to save  this recording to a binary file.
+1. Saved recordings can be reloaded  and the configuration commands replayed . This provides a means to easily reproduce a given sequence of configuration commands, or copy a saved configuration between compatible devices.
+1. Recorded commands of a similar type (UBX, NMEA or TTY) can also be imported  into PyGPSClient's json configuration file as [user defined presets](#user-defined-presets). They can then be replayed from the Presets panel via a single click.
+1. The Configuration Load facility can accept configuration files in either UBX/NMEA binary (\*.bin), TTY (\*.tty) or u-center UBX text format (\*.txt) (as also used by [Ardusimple](https://www.ardusimple.com/configuration-files/?wmc-currency=EUR)).
+1. Files saved using the [ubxsave](#ubxsave) CLI utility (*installed via the `pygnssutils` library*) can also be reloaded and replayed.
+
+**Tip:** The contents of a binary (\*.bin) config file can be reviewed using PyGPSClient's [file streaming facility](#filestream), *BUT* remember to set the `Msg Mode` in the Settings panel to `SET` rather than the default `GET` .
---
## NTRIP Client Facilities
@@ -318,7 +326,7 @@ By default, the server/caster binds to the host address '0.0.0.0' (IPv4) or '::'
1. Running in NTRIP CASTER mode is predicated on the host being connected to an RTK-compatible GNSS receiver **operating in Base Station mode** (either `FIXED` or `SURVEY_IN`) and outputting the requisite RTCM3 message types (1005/6, 1077, 1087, 1097, etc.).
1. It may be necessary to add a firewall rule and/or enable port-forwarding on the host machine or router to allow remote traffic on the specified address:port.
-1. The server supports encrypted TLS (HTTPS) connections. The TLS certificate/key location can be set via environment variable `PYGNSSUTILS_PEMPATH`; the default is `$HOME/pygnssutils.pem`. A self-signed pem file suitable for test and demonstration purposes can be created interactively thus:
+1. The server supports encrypted TLS (HTTPS) connections. The TLS server private key / certificate location can be set via environment variable `PYGNSSUTILS_PEMPATH`; the default is `$HOME/pygnssutils.pem`. A self-signed pem file suitable for test and demonstration purposes can be created interactively thus:
```shell
openssl req -x509 -newkey rsa:4096 -keyout pygnssutils.pem -out pygnssutils.pem -sha256 -days 3650 -nodes
```
@@ -407,10 +415,10 @@ If the command description contains the term `CONFIRM`, a pop-up confirmation bo
When PyGPSClient is first started, these settings are pre-populated with an initial set of preset commands, which can be saved to a \*.json configuration file and then manually removed, amended or supplemented in accordance with the user's preferences. To reinstate this initial set at a later date, insert the line `"INIT_PRESETS",` at the top of the relevant `"ubxpresets_l"`, `"nmeapresets_l"` or `"ttypresets_l"` configuration setting.
-The `pygpsclient.ubx2preset()` and `pygpsclient.nmea2preset()` helper functions may be used to convert a `UBXMessage` or `NMEAMessage` object into a preset string suitable for copying and pasting into the `"ubxpresents_l":` or `"nmeapresets_l":` JSON configuration sections:
+The `pygpsclient.ubx2preset()`, `pygpsclient.nmea2preset()` and `pygpsclient.tty2preset()` helper functions may be used to convert a `UBXMessage`, `NMEAMessage` or ASCII text object into a preset string suitable for copying and pasting into the `"ubxpresents_l":`, `"nmeapresets_l":` or `"ttypresets_l":` JSON configuration sections:
```python
-from pygpsclient import ubx2preset, nmea2preset
+from pygpsclient import ubx2preset, nmea2preset, tty2preset
from pyubx2 import UBXMessage
from pynmeagps import NMEAMessage, SET
@@ -419,14 +427,20 @@ print(ubx2preset(ubx, "Configure NAV-STATUS Message Rate on ZED-F9P"))
nmea = NMEAMessage("P", "QTMCFGUART", SET, baudrate=460800)
print(nmea2preset(nmea, "Configure UART baud rate on LG290P"))
+
+tty = b"AT+SYSTEM_RESET\r\n"
+print(tty2preset(tty, "IM19 System Reset CONFIRM"))
```
```
Configure NAV-STATUS Message Rate on ZED-F9P, CFG, CFG-MSG, 0103000100000000, 1
Configure UART baud rate on LG290P; P; QTMCFGUART; W,460800; 1
+IM19 System reset CONFIRM; AT+SYSTEM_RESET
```
Multiple commands can be concatenated on a single line. Illustrative examples are shown in the sample [pygpsclient.json](https://github.com/semuconsulting/PyGPSClient/blob/master/pygpsclient.json#L188) file.
+The [Configuration Command Load/Save/Record facility](#configuration-command-loadsaverecord-facility) can also be used to import recorded configuration command sequences into the presets section of the json configuration file.
+
---
## Command Line Utilities
diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md
index 0b335799..1a3eebb0 100644
--- a/RELEASE_NOTES.md
+++ b/RELEASE_NOTES.md
@@ -1,5 +1,13 @@
# PyGPSClient Release Notes
+### RELEASE 1.6.0
+
+1. Add user-selectable Signals widget, displaying individual GNSS PRN / Signal ID levels and (where applicable) correction sources (receiver must support UBX NAV-SIG messages). Provides greater granularity than the standard Levels widget. Signal IDs are shown in RINEX format e.g. "L1_C/A", "E5_aQ", etc.
+1. Add user-defined preset import facility to Configuration Load/Save/Record panel. This allows user to record a sequence of UBX, NMEA or TTY commands as they are sent to the receiver and to import this sequence as a user-defined preset in the PyGPSClient json configuration file. This obviates the need to edit the configuration file manually. Remember to re-save the configuration file to persist the changes.
+1. NTRIP Caster / Socket Server Configuration is now a separate Toplevel dialog panel, accessed through Server Config button or Menu Option Server Configuration. Number of connected clients is now displayed in topmost banner panel.
+1. Show "C/No = 0 dbHz" option ("unused satellites") is now accessible through double-right-click on LevelsView and SignalsView widgets; option removed from main Settings panel.
+1. Minor cosmetic updates to SpectrumView widget and Settings panel.
+
### RELEASE 1.5.23
FIXES:
diff --git a/docs/pygpsclient.rst b/docs/pygpsclient.rst
index a1b5c8a3..fd8231db 100644
--- a/docs/pygpsclient.rst
+++ b/docs/pygpsclient.rst
@@ -300,10 +300,10 @@ pygpsclient.serialconfig\_lband\_frame module
:undoc-members:
:show-inheritance:
-pygpsclient.serverconfig\_frame module
---------------------------------------
+pygpsclient.serverconfig\_dialog module
+---------------------------------------
-.. automodule:: pygpsclient.serverconfig_frame
+.. automodule:: pygpsclient.serverconfig_dialog
:members:
:undoc-members:
:show-inheritance:
@@ -316,6 +316,14 @@ pygpsclient.settings\_frame module
:undoc-members:
:show-inheritance:
+pygpsclient.signalsview\_frame module
+-------------------------------------
+
+.. automodule:: pygpsclient.signalsview_frame
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
pygpsclient.skyview\_frame module
---------------------------------
diff --git a/images/app.png b/images/app.png
index b40123df..355a7f99 100644
Binary files a/images/app.png and b/images/app.png differ
diff --git a/images/recorder_dialog.png b/images/recorder_dialog.png
index aa058e4f..45fd5f9f 100644
Binary files a/images/recorder_dialog.png and b/images/recorder_dialog.png differ
diff --git a/images/signalsview_widget.png b/images/signalsview_widget.png
new file mode 100644
index 00000000..009976d7
Binary files /dev/null and b/images/signalsview_widget.png differ
diff --git a/pyproject.toml b/pyproject.toml
index 772fbbeb..ec601ae8 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -129,12 +129,13 @@ disable = """
too-many-public-methods,
too-many-locals,
invalid-name,
- logging-fstring-interpolation
+ logging-fstring-interpolation,
+ fixme,
"""
[tool.pytest.ini_options]
minversion = "7.0"
-addopts = "--cov --cov-report html --cov-fail-under 18"
+addopts = "--cov --cov-report html --cov-fail-under 17"
pythonpath = ["src"]
testpaths = ["tests"]
@@ -145,7 +146,7 @@ source = ["src"]
source = ["src"]
[tool.coverage.report]
-fail_under = 18
+fail_under = 17
[tool.coverage.html]
directory = "htmlcov"
diff --git a/src/pygpsclient/__init__.py b/src/pygpsclient/__init__.py
index 9188e7c8..1e3bc118 100644
--- a/src/pygpsclient/__init__.py
+++ b/src/pygpsclient/__init__.py
@@ -9,7 +9,7 @@
# pylint: disable=invalid-name
from pygpsclient._version import __version__
-from pygpsclient.helpers import nmea2preset, ubx2preset
+from pygpsclient.helpers import nmea2preset, tty2preset, ubx2preset
from pygpsclient.sqlite_handler import retrieve_data
version = __version__
diff --git a/src/pygpsclient/__main__.py b/src/pygpsclient/__main__.py
index cbd2b861..1f6eec7d 100644
--- a/src/pygpsclient/__main__.py
+++ b/src/pygpsclient/__main__.py
@@ -102,6 +102,16 @@ def main():
type=int,
default=SUPPRESS,
)
+ ap.add_argument(
+ "--tlspempath",
+ help="Fully qualified path to TLS PEM (private key/certificate) file",
+ default=SUPPRESS,
+ )
+ ap.add_argument(
+ "--tlscrtpath",
+ help="Fully qualified path to TLS CRT (certificate) file",
+ default=SUPPRESS,
+ )
ap.add_argument(
"--verbosity",
help=(
diff --git a/src/pygpsclient/_version.py b/src/pygpsclient/_version.py
index 0be58edd..376e36fd 100644
--- a/src/pygpsclient/_version.py
+++ b/src/pygpsclient/_version.py
@@ -8,4 +8,4 @@
:license: BSD 3-Clause
"""
-__version__ = "1.5.23"
+__version__ = "1.6.0"
diff --git a/src/pygpsclient/app.py b/src/pygpsclient/app.py
index 4df2a2a3..b606a2be 100644
--- a/src/pygpsclient/app.py
+++ b/src/pygpsclient/app.py
@@ -176,6 +176,7 @@ def __init__(self, master, **kwargs): # pylint: disable=too-many-statements
self.__master.iconphoto(True, PhotoImage(file=ICON_APP128))
self._deferredmsg = None
+ self._server_status = -1 # socket server status -1 = inactive
self.gnss_inqueue = Queue() # messages from GNSS receiver
self.gnss_outqueue = Queue() # messages to GNSS receiver
self.ntrip_inqueue = Queue() # messages from NTRIP source
@@ -465,7 +466,6 @@ def load_config(self):
self.frm_settings,
self.frm_settings.frm_serial,
self.frm_settings.frm_socketclient,
- self.frm_settings.frm_socketserver,
):
frm.reset()
self._do_layout()
@@ -560,6 +560,8 @@ def sockserver_start(self):
https = cfg.get("sockhttps_b")
ntripuser = cfg.get("ntripcasteruser_s")
ntrippassword = cfg.get("ntripcasterpassword_s")
+ tlspempath = cfg.get("tlspempath_s")
+ ntriprtcmstr = "1002(1),1006(5),1077(1),1087(1),1097(1),1127(1),1230(1)" # TODO
self._socket_thread = Thread(
target=self._sockserver_thread,
args=(
@@ -567,6 +569,8 @@ def sockserver_start(self):
host,
port,
https,
+ tlspempath,
+ ntriprtcmstr,
ntripuser,
ntrippassword,
SOCKSERVER_MAX_CLIENTS,
@@ -575,16 +579,16 @@ def sockserver_start(self):
daemon=True,
)
self._socket_thread.start()
- self.frm_banner.update_transmit_status(0)
+ self.server_status = 0 # 0 = active, no clients
def sockserver_stop(self):
"""
Stop socket server thread.
"""
- self.frm_banner.update_transmit_status(-1)
if self._socket_server is not None:
self._socket_server.shutdown()
+ self.server_status = -1 # -1 = inactive
def _sockserver_thread(
self,
@@ -592,6 +596,8 @@ def _sockserver_thread(
host: str,
port: int,
https: int,
+ tlspempath: str,
+ ntriprtcmstr: str,
ntripuser: str,
ntrippassword: str,
maxclients: int,
@@ -605,6 +611,8 @@ def _sockserver_thread(
:param str host: socket host name (0.0.0.0)
:param int port: socket port (50010)
:param int https: https enabled (0)
+ :param str tlspempath: path to TLS PEM file ("$HOME/pygnssutils.pem")
+ :param str ntriprtcmstr: NTRIP caster RTCM type(rate) sourcetable entry
:param int maxclients: max num of clients (5)
:param Queue socketqueue: socket server read queue
"""
@@ -620,6 +628,8 @@ def _sockserver_thread(
requesthandler,
ntripuser=ntripuser,
ntrippassword=ntrippassword,
+ tlspempath=tlspempath,
+ ntriprtcmstr=ntriprtcmstr,
) as self._socket_server:
self._socket_server.serve_forever()
except OSError as err:
@@ -633,7 +643,8 @@ def update_clients(self, clients: int):
:param int clients: no of connected clients
"""
- self.frm_settings.frm_socketserver.clients = clients
+ self.server_status = clients
+ # self.frm_settings.frm_socketserver.clients = clients TODO
def _shutdown(self):
"""
@@ -684,9 +695,9 @@ def on_gnss_read(self, event): # pylint: disable=unused-argument
raw_data, parsed_data = self.gnss_inqueue.get(False)
if raw_data is not None and parsed_data is not None:
self.process_data(raw_data, parsed_data)
- # if socket server is running, output raw data to socket
- if self.frm_settings.frm_socketserver.socketserving:
- self.socket_outqueue.put(raw_data)
+ # if socket server is running, output raw data to socket
+ if self.server_status: # TODO
+ self.socket_outqueue.put(raw_data)
self.gnss_inqueue.task_done()
except Empty:
pass
@@ -699,9 +710,7 @@ def on_gnss_eof(self, event): # pylint: disable=unused-argument
:param event event: <> event
"""
- self.frm_settings.frm_socketserver.socketserving = (
- False # turn off socket server
- )
+ self.server_status = -1
self._refresh_widgets()
self.conn_status = DISCONNECTED
self.status_label = (ENDOFFILE, ERRCOL)
@@ -714,9 +723,7 @@ def on_gnss_timeout(self, event): # pylint: disable=unused-argument
:param event event: <> event
"""
- self.frm_settings.frm_socketserver.socketserving = (
- False # turn off socket server
- )
+ self.server_status = -1
self._refresh_widgets()
self.conn_status = DISCONNECTED
self.status_label = (INACTIVE_TIMEOUT, ERRCOL)
@@ -1078,6 +1085,30 @@ def conn_status(self, status: int):
if status == DISCONNECTED:
self.conn_label = (NOTCONN, INFOCOL)
+ @property
+ def server_status(self) -> int:
+ """
+ Getter for socket server status.
+
+ :return: server status
+ :rtype: int
+ """
+
+ return self._server_status
+
+ @server_status.setter
+ def server_status(self, status: int):
+ """
+ Setter for socket server status.
+
+ :param int status: server status
+ -1 - inactive, 0 = active no clients, >0 = active clients
+ """
+
+ self._server_status = status
+ self.frm_banner.update_transmit_status(status)
+ self.configuration.set("sockserver_b", status >= 0)
+
@property
def rtk_conn_status(self) -> int:
"""
diff --git a/src/pygpsclient/banner_frame.py b/src/pygpsclient/banner_frame.py
index b1945a59..86e2a1c3 100644
--- a/src/pygpsclient/banner_frame.py
+++ b/src/pygpsclient/banner_frame.py
@@ -170,7 +170,7 @@ def _body(self):
)
self._lbl_ldgps = Label(
self._frm_advanced2,
- text="dgps:",
+ text="corr:",
bg=self._bgcol,
fg=self._fgcol,
anchor=N,
@@ -186,6 +186,9 @@ def _body(self):
self._lbl_transmit_preset = Label(
self._frm_connect, bg=self._bgcol, image=self._img_blank
)
+ self._lbl_clients = Label(
+ self._frm_connect, bg=self._bgcol, fg="green", width=2, anchor=W
+ )
self._lbl_time = Label(
self._frm_basic, bg=self._bgcol, fg="cyan", width=15, anchor=W
@@ -252,7 +255,8 @@ def _do_layout(self):
self._lbl_status_preset.grid(column=0, row=0, padx=2, pady=3, sticky=W)
self._lbl_rtk_preset.grid(column=1, row=0, padx=2, pady=3, sticky=W)
- self._lbl_transmit_preset.grid(column=2, row=0, padx=2, pady=3, sticky=W)
+ self._lbl_transmit_preset.grid(column=2, row=0, padx=1, pady=3, sticky=W)
+ self._lbl_clients.grid(column=3, row=0, padx=1, pady=3, sticky=W)
self._lbl_ltime.grid(column=1, row=0, pady=0, padx=0, sticky=W)
self._lbl_time.grid(column=2, row=0, pady=0, padx=0, sticky=W)
self._lbl_llat.grid(column=3, row=0, pady=0, padx=0, sticky=W)
@@ -358,10 +362,13 @@ def update_transmit_status(self, transmit: int = 1):
if transmit > 0:
self._lbl_transmit_preset.configure(image=self._img_transmit)
+ self._lbl_clients.config(text=transmit, fg="#6b8839")
elif transmit == 0:
self._lbl_transmit_preset.configure(image=self._img_noclient)
+ self._lbl_clients.config(text=transmit, fg="#e7b03e")
else:
self._lbl_transmit_preset.configure(image=self._img_blank)
+ self._lbl_clients.config(text=" ", fg=BGCOL)
def update_frame(self):
"""
diff --git a/src/pygpsclient/canvas_plot.py b/src/pygpsclient/canvas_plot.py
index 7f608bb0..bac819c4 100644
--- a/src/pygpsclient/canvas_plot.py
+++ b/src/pygpsclient/canvas_plot.py
@@ -69,6 +69,7 @@ def create_graph(
xcol: str = "#000000",
ycol: tuple = ("#000000",),
xlabels: bool = False,
+ xlabelsfrm: str = "000",
ylabels: bool = False,
fontscale: int = 30,
**kwargs,
@@ -77,26 +78,27 @@ def create_graph(
Extends tkinter.Canvas Class to simplify drawing graphs on canvas.
Accommodates multiple Y axis channels.
- :param float xdatamax: x maximum data value,
- :param float xdatamin: x minimum data value,
- :param tuple ydatamax: y channel(s) maximum data value,
- :param tuple ydatamin: y channel(s) minimum data value,
- :param int xtickmaj: x major ticks,
+ :param float xdatamax: x maximum data value
+ :param float xdatamin: x minimum data value
+ :param tuple ydatamax: y channel(s) maximum data value
+ :param tuple ydatamin: y channel(s) minimum data value
+ :param int xtickmaj: x major ticks
:param int ytickmaj: y major ticks
- :param int xtickmin: x minor ticks,
- :param int ytickmin: y minor ticks,
- :param str fillmaj: major axis color,
- :param str fillmin: minor axis color,
- :param int xdp: x label decimal places,
- :param tuple ydp: y channel(s) label decimal places,
- :param str xlegend: x legend,
+ :param int xtickmin: x minor ticks
+ :param int ytickmin: y minor ticks
+ :param str fillmaj: major axis color
+ :param str fillmin: minor axis color
+ :param int xdp: x label decimal places
+ :param tuple ydp: y channel(s) label decimal places
+ :param str xlegend: x legend
:param str xtimeformat: x label time format e.g. "%H:%M:%S"
- :param tuple ylegend: y channels legend,
- :param str xcol: x label color,
- :param tuple ycol: y channel(s) color,
- :param bool xlabels: x labels on/off,
- :param bool ylabels: y labels on/off,
- :param int fontscale: font scaling factor (higher is smaller),
+ :param tuple ylegend: y channels legend
+ :param str xcol: x label color
+ :param tuple ycol: y channel(s) color
+ :param bool xlabels: x labels on/off
+ :param str xlabelsfrm: xlabel format string e.g. "000"
+ :param bool ylabels: y labels on/off
+ :param int fontscale: font scaling factor (higher is smaller)
:return: return code
:rtype: int
:raises: ValueError if Y channel args have dissimilar lengths
@@ -140,10 +142,11 @@ def linspace(num: int, start: float, stop: float):
self.fnth = self.font.metrics("linespace")
self.xoffl = self.fnth * ceil(len(ydatamax) / 2) * 1.5
self.xoffr = self.xoffl
- self.yoffb = self.fnth * 1.5
xangle = kwargs.pop("xangle", 0)
- if xangle != 0: # add extra Y offset for slanted X labels
- self.yoffb += self.font.measure("000") * sin(radians(xangle))
+ if xangle == 0:
+ self.yoffb = self.fnth * 1.5
+ else: # add extra Y offset for slanted X labels
+ self.yoffb = self.font.measure(xlabelsfrm) * cos(radians(xangle)) * 1.2
self.yofft = self.fnth
self.xdatamax = xdatamax
self.xdatamin = xdatamin
diff --git a/src/pygpsclient/configuration.py b/src/pygpsclient/configuration.py
index dfe078e3..5ed8e4af 100644
--- a/src/pygpsclient/configuration.py
+++ b/src/pygpsclient/configuration.py
@@ -16,6 +16,12 @@
from os import getenv
from types import NoneType
+from pygnssutils import (
+ PYGNSSUTILS_CRT,
+ PYGNSSUTILS_CRTPATH,
+ PYGNSSUTILS_PEM,
+ PYGNSSUTILS_PEMPATH,
+)
from pyubx2 import GET
from serial import PARITY_NONE
@@ -139,6 +145,8 @@ def __init__(self, app):
"trackpath_s": "",
"database_b": 0,
"databasepath_s": "",
+ "tlspempath_s": PYGNSSUTILS_PEM,
+ "tlscrtpath_s": PYGNSSUTILS_CRT,
# serial port settings from frm_serial
"serialport_s": "/dev/ttyACM0",
"bpsrate_n": 9600,
@@ -346,6 +354,12 @@ def loadcli(self, **kwargs):
arg = kwargs.pop("ntripcasterpassword", getenv("NTRIPCASTER_PASSWORD", None))
if arg is not None:
self.set("ntripcasterpassword_s", arg)
+ arg = kwargs.pop("tlspempath", getenv(PYGNSSUTILS_PEMPATH, PYGNSSUTILS_PEM))
+ if arg is not None:
+ self.set("tlspempath_s", arg)
+ arg = kwargs.pop("tlscrtpath", getenv(PYGNSSUTILS_CRTPATH, PYGNSSUTILS_CRT))
+ if arg is not None:
+ self.set("tlscrtpath_s", arg)
def set(self, name: str, value: object):
"""
diff --git a/src/pygpsclient/dialog_state.py b/src/pygpsclient/dialog_state.py
index ce863981..674e4571 100644
--- a/src/pygpsclient/dialog_state.py
+++ b/src/pygpsclient/dialog_state.py
@@ -23,6 +23,7 @@
from pygpsclient.nmea_config_dialog import NMEAConfigDialog
from pygpsclient.ntrip_client_dialog import NTRIPConfigDialog
from pygpsclient.recorder_dialog import RecorderDialog
+from pygpsclient.serverconfig_dialog import ServerConfigDialog
from pygpsclient.spartn_dialog import SPARTNConfigDialog
from pygpsclient.strings import (
DLG,
@@ -32,6 +33,7 @@
DLGTNMEA,
DLGTNTRIP,
DLGTRECORD,
+ DLGTSERVER,
DLGTSPARTN,
DLGTTTY,
DLGTUBX,
@@ -71,6 +73,11 @@ def __init__(self):
DLG: None,
RESIZE: False,
},
+ DLGTSERVER: {
+ CLASS: ServerConfigDialog,
+ DLG: None,
+ RESIZE: False,
+ },
DLGTSPARTN: {
CLASS: SPARTNConfigDialog,
DLG: None,
diff --git a/src/pygpsclient/globals.py b/src/pygpsclient/globals.py
index 1f44abcd..6fdd3a29 100644
--- a/src/pygpsclient/globals.py
+++ b/src/pygpsclient/globals.py
@@ -170,6 +170,7 @@ def create_circle(self: Canvas, x: int, y: int, r: int, **kwargs):
ICON_EXIT = path.join(DIRNAME, "resources/iconmonstr-door-6-24.png")
ICON_EXPAND = path.join(DIRNAME, "resources/iconmonstr-arrow-80-16.png")
ICON_GITHUB = path.join(DIRNAME, "resources/github-256.png")
+ICON_IMPORT = path.join(DIRNAME, "resources/iconmonstr-import-24.png")
ICON_LOAD = path.join(DIRNAME, "resources/iconmonstr-folder-18-24.png")
ICON_LOGREAD = path.join(DIRNAME, "resources/binary-1-24.png")
ICON_NMEACONFIG = path.join(DIRNAME, "resources/iconmonstr-gear-2-24-nmea.png")
@@ -335,6 +336,7 @@ def create_circle(self: Canvas, x: int, y: int, r: int, **kwargs):
NMEA_CFGOTHER = 17
SERVERCONFIG = 18
SBF_MONHW = 19
+SIGNALSVIEW = 20
KNOWNGPS = (
"cp210",
diff --git a/src/pygpsclient/gnss_status.py b/src/pygpsclient/gnss_status.py
index fb429463..b6a1352e 100644
--- a/src/pygpsclient/gnss_status.py
+++ b/src/pygpsclient/gnss_status.py
@@ -56,6 +56,9 @@ def __init__(self):
self.rel_pos_flags = [] # rover relative position flags
# dict of satellite {(gnssid,svid}: (gnssId, svid, elev, azim, cno, last_updated)}
self.gsv_data = {}
+ # dict of signal {(gnssid,svid,sigid}: (gnssId, svid, sigid, cno, corrsource, quality,
+ # sigflags, last_updated)}
+ self.sig_data = {}
# dict of hardware, firmware and software versions
self.version_data = {
"swversion": NA,
diff --git a/src/pygpsclient/helpers.py b/src/pygpsclient/helpers.py
index 70f0733f..ab88a68d 100644
--- a/src/pygpsclient/helpers.py
+++ b/src/pygpsclient/helpers.py
@@ -35,11 +35,11 @@
Spinbox,
StringVar,
Tk,
+ font,
)
-from tkinter.font import Font
from typing import Literal
-from pynmeagps import WGS84_SMAJ_AXIS, haversine
+from pynmeagps import WGS84_SMAJ_AXIS, NMEAMessage, haversine
from pyubx2 import (
SET,
SET_LAYER_RAM,
@@ -53,6 +53,7 @@
from requests import get
from pygpsclient.globals import (
+ BSR,
ERRCOL,
FIXLOOKUP,
GPSEPOCH0,
@@ -389,6 +390,40 @@ def dop2str(dop: float) -> str:
return dops
+def fitfont(
+ fmt: str,
+ maxw: int,
+ maxh: int,
+ angle: int = 0,
+ maxsiz: int = 10,
+ constraint: int = 3,
+) -> tuple[font.Font, float, float]:
+ """
+ Create font to fit space.
+
+ :param str format: format of string
+ :param int maxw: max width in pixels
+ :param int maxh: max height in pixels
+ :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]
+ """
+
+ fw, fh = maxw + 1, maxh + 1
+ rw, rh = fw, fh
+ siz = maxsiz
+ fnt = font.Font(size=-siz)
+ while (
+ (rw > maxw and constraint & 1) or (rh > maxh and constraint & 2)
+ ) and siz > 0:
+ fnt = font.Font(size=-siz)
+ rw, rh = fontdim(fmt, fnt, angle)
+ siz -= 1
+ return fnt, fw, fh
+
+
def fix2desc(msgid: str, fix: object) -> str:
"""
Get integer fix value for given message fix status.
@@ -418,6 +453,25 @@ def ft2m(feet: float) -> float:
return feet / 3.28084
+def fontdim(fmt: str, fnt: font.Font, angle: int = 0) -> tuple[float, float]:
+ """
+ Get x,y pixel dimensions of string in given rotated font.
+
+ :param str fmt: format string e.g. "000"
+ :param font.Font fnt: font
+ :param int angle: rotation angle in degrees (0 = horizontal)
+ :return: tuple of (width, height)
+ :rtype: tuple[float, float]
+ """
+
+ theta = radians(angle)
+ fw = fnt.measure(fmt)
+ fh = fnt.metrics("linespace")
+ rw = abs(fw * cos(theta)) + abs(fh * sin(theta))
+ rh = abs(fh * cos(theta)) + abs(fw * sin(theta))
+ return rw, rh
+
+
def get_mp_distance(lat: float, lon: float, mp: list) -> float:
"""
Get distance to mountpoint from current location (if known).
@@ -802,7 +856,9 @@ def ned2vector(n: float, e: float, d: float) -> tuple:
return dis, hdg
-def nmea2preset(msgs: tuple, desc: str = "") -> str:
+def nmea2preset(
+ msgs: NMEAMessage | tuple[NMEAMessage] | list[NMEAMessage], desc: str = ""
+) -> str:
"""
Convert one or more NMEAMessages to format suitable for adding to user-defined
preset list `nmeapresets_l` in PyGPSClient .json configuration files.
@@ -812,14 +868,14 @@ def nmea2preset(msgs: tuple, desc: str = "") -> str:
e.g. "Configure Signals; P; QTMCFGSIGNAL; W,7,3,F,3F,7,1; 1"
- :param tuple msgs: NMEAmessage or tuple of NMEAmessages
+ :param NMEAMessage | tuple[NMEAMessage] | list[NMEAMessage] msgs: NMEAmessage(s)
:param str desc: preset description
:return: preset string
:rtype: str
"""
desc = desc.replace(";", " ")
- if not isinstance(msgs, tuple):
+ if not isinstance(msgs, (tuple, list)):
msgs = (msgs,)
preset = (
f"{msgs[0].identity} {['GET','SET','POLL'][msgs[0].msgmode]}"
@@ -985,7 +1041,7 @@ def rgb2str(r: int, g: int, b: int) -> str:
def scale_font(
- width: int, basesize: int, txtwidth: int, maxsize: int = 0, fnt: Font = None
+ width: int, basesize: int, txtwidth: int, maxsize: int = 0, fnt: font.Font = None
) -> tuple:
"""
Scale font size to widget width.
@@ -999,9 +1055,9 @@ def scale_font(
:rtype: tuple
"""
- fnt = Font(size=12) if fnt is None else fnt
+ fnt = font.Font(size=12) if fnt is None else fnt
fs = basesize * width / fnt.measure("W" * txtwidth)
- fnt = Font(size=int(min(fs, maxsize))) if maxsize else Font(size=int(fs))
+ fnt = font.Font(size=int(min(fs, maxsize))) if maxsize else font.Font(size=int(fs))
return fnt, fnt.metrics("linespace")
@@ -1238,6 +1294,32 @@ def time2str(tim: float, sformat: str = "%H:%M:%S") -> str:
return dt.strftime(sformat)
+def tty2preset(msgs: bytes | tuple[bytes] | list[bytes], desc: str = "") -> str:
+ """
+ Convert one or more ASCII TTY commands to format suitable for adding to user-defined
+ preset list `ttypresets_l` in PyGPSClient .json configuration files.
+
+ The format is:
+ "; "
+
+ e.g. "IM19 System reset CONFIRM; AT+SYSTEM_RESET"
+
+ :param bytes | tuple[bytes] | list[bytes] msgs: ASCII TTY command(s)
+ :param str desc: preset description
+ :return: preset string
+ :rtype: str
+ """
+
+ desc = desc.replace(";", " ")
+ if not isinstance(msgs, (tuple, list)):
+ msgs = (msgs,)
+ preset = "TTY Command" if desc == "" else desc
+ for msg in msgs:
+ cmd = msg.decode("ascii", errors=BSR).strip("\r\n")
+ preset += f"; {cmd}"
+ return preset
+
+
def unused_sats(data: dict) -> int:
"""
Get number of 'unused' sats in gnss_data.gsv_data.
@@ -1250,7 +1332,9 @@ def unused_sats(data: dict) -> int:
return sum(1 for (_, _, _, _, cno, _) in data.values() if cno == 0)
-def ubx2preset(msgs: tuple, desc: str = "") -> str:
+def ubx2preset(
+ msgs: UBXMessage | tuple[UBXMessage] | list[UBXMessage], desc: str = ""
+) -> str:
"""
Convert one or more UBXMessages to format suitable for adding to user-defined
preset list `ubxpresets_l` in PyGPSClient .json configuration files.
@@ -1260,14 +1344,14 @@ def ubx2preset(msgs: tuple, desc: str = "") -> str:
e.g. "Set NMEA High Precision Mode, CFG, CFG-VALSET, 000100000600931001, 1"
- :param tuple msgs: UBXMessage or tuple of UBXmessages
+ :param UBXMessage | tuple[UBXMessage] | list[UBXMessage] msgs: UBXMessage(s)
:param str desc: preset description
:return: preset string
:rtype: str
"""
desc = desc.replace(",", " ")
- if not isinstance(msgs, tuple):
+ if not isinstance(msgs, (tuple, list)):
msgs = (msgs,)
preset = (
f"{msgs[0].identity} {['GET','SET','POLL'][msgs[0].msgmode]}"
diff --git a/src/pygpsclient/levelsview_frame.py b/src/pygpsclient/levelsview_frame.py
index e263e972..1e2a97f0 100644
--- a/src/pygpsclient/levelsview_frame.py
+++ b/src/pygpsclient/levelsview_frame.py
@@ -31,11 +31,12 @@
MAX_SNR,
WIDGETU2,
)
-from pygpsclient.helpers import col2contrast, unused_sats
+from pygpsclient.helpers import col2contrast, fitfont, unused_sats
OL_WID = 1
FONTSCALELG = 40
-FONTSCALESV = 30
+XLBLANGLE = 35
+XLBLFMT = "000"
class LevelsviewFrame(Frame):
@@ -83,6 +84,8 @@ def _attach_events(self):
self.bind("", self._on_resize)
self._canvas.bind("", self._on_legend)
+ self._canvas.bind("", self._on_cno0)
+ self._canvas.bind("", self._on_cno0)
def _on_legend(self, event): # pylint: disable=unused-argument
"""
@@ -96,6 +99,18 @@ def _on_legend(self, event): # pylint: disable=unused-argument
)
self._redraw = True
+ def _on_cno0(self, event): # pylint: disable=unused-argument
+ """
+ On double-right-click - include levels where C/No = 0.
+
+ :param event: event
+ """
+
+ self.__app.configuration.set(
+ "unusedsat_b", not self.__app.configuration.get("unusedsat_b")
+ )
+ self._redraw = True
+
def init_frame(self):
"""
Initialise graph view
@@ -111,15 +126,13 @@ def init_frame(self):
ylegend=("C/No dBHz",),
ycol=(FGCOL,),
ylabels=True,
- xangle=35,
+ xlabelsfrm=XLBLFMT,
+ xangle=XLBLANGLE,
fontscale=FONTSCALELG,
tags=tags,
)
self._redraw = False
- if self.__app.configuration.get("legend_b"):
- self._draw_legend()
-
def _draw_legend(self):
"""
Draw GNSS color code legend
@@ -166,21 +179,15 @@ def update_frame(self):
w, h = self.width, self.height
self.init_frame()
- offset = self._canvas.xoffl # AXIS_XL + 2
+ offset = self._canvas.xoffl
colwidth = (w - self._canvas.xoffl - self._canvas.xoffr + 1) / siv
- # scale x axis label
- fsiz = min(w * 15 / siv, w, h)
- svfont = font.Font(size=int(fsiz / FONTSCALESV))
+ 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:
- if show_unused:
- cno = 1 # show 'place marker' in graph
- else:
- continue
+ if cno == 0 and not show_unused:
+ continue
snr_y = int(cno) * (h - self._canvas.yoffb - 1) / MAX_SNR
(_, ol_col) = GNSS_LIST[gnssId]
- prn = f"{int(prn):02}"
self._canvas.create_rectangle(
offset,
h - self._canvas.yoffb - 1,
@@ -194,16 +201,18 @@ def update_frame(self):
self._canvas.create_text(
offset + colwidth / 2,
h - self._canvas.yoffb - 1,
- text=prn,
+ text=f"{int(prn):02}",
fill=FGCOL,
- font=svfont,
- angle=35,
+ font=xfnt,
+ angle=XLBLANGLE,
anchor=NE,
tags=TAG_DATA,
)
offset += colwidth
- self.update_idletasks()
+ if self.__app.configuration.get("legend_b"):
+ self._draw_legend()
+ self.update_idletasks()
def _on_resize(self, event): # pylint: disable=unused-argument
"""
diff --git a/src/pygpsclient/menu_bar.py b/src/pygpsclient/menu_bar.py
index f597fc81..ff46f615 100644
--- a/src/pygpsclient/menu_bar.py
+++ b/src/pygpsclient/menu_bar.py
@@ -22,6 +22,7 @@
DLGTNMEA,
DLGTNTRIP,
DLGTRECORD,
+ DLGTSERVER,
DLGTTTY,
DLGTUBX,
MENUABOUT,
@@ -40,6 +41,7 @@
DLGTUBX,
DLGTNMEA,
DLGTNTRIP,
+ DLGTSERVER,
DLGTSPARTN, # service discontinued by u-blox
DLGTGPX,
DLGTIMPORTMAP,
diff --git a/src/pygpsclient/recorder_dialog.py b/src/pygpsclient/recorder_dialog.py
index c3db5009..72665942 100644
--- a/src/pygpsclient/recorder_dialog.py
+++ b/src/pygpsclient/recorder_dialog.py
@@ -18,6 +18,7 @@
# pylint: disable=unused-argument
+from datetime import datetime
from threading import Event, Thread
from time import sleep
from tkinter import CENTER, EW, NSEW, Button, Frame, Label, TclError, W, filedialog
@@ -45,6 +46,7 @@
FGCOL,
HOME,
ICON_DELETE,
+ ICON_IMPORT,
ICON_LOAD,
ICON_RECORD,
ICON_SAVE,
@@ -56,7 +58,7 @@
PNTCOL,
UNDO,
)
-from pygpsclient.helpers import set_filename
+from pygpsclient.helpers import nmea2preset, set_filename, tty2preset, ubx2preset
from pygpsclient.strings import DLGTRECORD, SAVETITLE
from pygpsclient.toplevel_dialog import ToplevelDialog
@@ -100,6 +102,7 @@ def __init__(self, app, *args, **kwargs):
self._img_play = ImageTk.PhotoImage(Image.open(ICON_SEND))
self._img_stop = ImageTk.PhotoImage(Image.open(ICON_STOP))
self._img_record = ImageTk.PhotoImage(Image.open(ICON_RECORD))
+ self._img_import = ImageTk.PhotoImage(Image.open(ICON_IMPORT))
self._img_undo = ImageTk.PhotoImage(Image.open(ICON_UNDO))
self._img_delete = ImageTk.PhotoImage(Image.open(ICON_DELETE))
self._rec_status = STOP
@@ -138,6 +141,14 @@ def _body(self):
highlightbackground=BGCOL,
highlightthickness=2,
)
+ self._btn_import = Button(
+ self._frm_body,
+ image=self._img_import,
+ width=40,
+ command=self._on_import,
+ highlightbackground=BGCOL,
+ highlightthickness=2,
+ )
self._btn_play = Button(
self._frm_body,
image=self._img_play,
@@ -191,11 +202,12 @@ def _do_layout(self):
self._frm_body.grid(column=0, row=0, sticky=NSEW)
self._btn_load.grid(column=0, row=0, ipadx=3, ipady=3, sticky=W)
self._btn_save.grid(column=1, row=0, ipadx=3, ipady=3, sticky=W)
- self._btn_play.grid(column=2, row=0, ipadx=3, ipady=3, sticky=W)
- self._btn_record.grid(column=3, row=0, ipadx=3, ipady=3, sticky=W)
- self._btn_undo.grid(column=4, row=0, ipadx=3, ipady=3, sticky=W)
- self._btn_delete.grid(column=5, row=0, ipadx=3, ipady=3, sticky=W)
- self._lbl_memory.grid(column=6, row=0, ipadx=3, ipady=3, sticky=W)
+ self._btn_import.grid(column=2, row=0, ipadx=3, ipady=3, sticky=W)
+ self._btn_play.grid(column=3, row=0, ipadx=3, ipady=3, sticky=W)
+ self._btn_record.grid(column=4, row=0, ipadx=3, ipady=3, sticky=W)
+ self._btn_undo.grid(column=5, row=0, ipadx=3, ipady=3, sticky=W)
+ self._btn_delete.grid(column=6, row=0, ipadx=3, ipady=3, sticky=W)
+ self._lbl_memory.grid(column=7, row=0, ipadx=3, ipady=3, sticky=W)
self._lbl_activity.grid(column=0, row=2, columnspan=7, padx=3, sticky=EW)
(cols, rows) = self.grid_size()
@@ -451,6 +463,49 @@ def _on_record(self):
self.status_label = (f"Recording {stat}", INFOCOL)
self._update_status()
+ def _on_import(self):
+ """
+ Import commands as presets.
+
+ NB: Assumes all commands in a single recording are of the
+ same type (i.e. UBX, NMEA or TTY).
+ """
+
+ if self._rec_status == RECORD:
+ return
+
+ if len(self.__app.recorded_commands) == 0:
+ self.status_label = ("Nothing to import", ERRCOL)
+ return
+
+ try:
+ now = f'Recorded commands {datetime.now().strftime("%Y-%m-%d_%H:%M:%S")}'
+ if isinstance(self.__app.recorded_commands[0], UBXMessage):
+ self.__app.configuration.get("ubxpresets_l").append(
+ ubx2preset(self.__app.recorded_commands, now)
+ )
+ typ = "UBX"
+ elif isinstance(self.__app.recorded_commands[0], NMEAMessage):
+ self.__app.configuration.get("nmeapresets_l").append(
+ nmea2preset(self.__app.recorded_commands, now)
+ )
+ typ = "NMEA"
+ else: # tty
+ self.__app.configuration.get("ttypresets_l").append(
+ tty2preset(self.__app.recorded_commands, now)
+ )
+ typ = "TTY"
+
+ self.status_label = (
+ f"{len(self.__app.recorded_commands)} commands imported as {typ} presets",
+ OKCOL,
+ )
+ except AttributeError:
+ self.status_label = (
+ "Recorded commands must be of same type",
+ ERRCOL,
+ )
+
def _on_undo(self):
"""
Remove last record from in-memory recording.
diff --git a/src/pygpsclient/resources/iconmonstr-import-24.png b/src/pygpsclient/resources/iconmonstr-import-24.png
new file mode 100644
index 00000000..641dec0a
Binary files /dev/null and b/src/pygpsclient/resources/iconmonstr-import-24.png differ
diff --git a/src/pygpsclient/rtcm3_handler.py b/src/pygpsclient/rtcm3_handler.py
index e12624e9..a9369ead 100644
--- a/src/pygpsclient/rtcm3_handler.py
+++ b/src/pygpsclient/rtcm3_handler.py
@@ -84,5 +84,5 @@ def _process_1005(self, parsed: RTCMMessage):
# update Survey-In base station location
if self.__app.frm_settings.frm_socketserver is not None:
self.__app.frm_settings.frm_socketserver.update_base_location()
- except (AttributeError, ValueError):
+ except (AttributeError, TypeError, ValueError):
pass
diff --git a/src/pygpsclient/serialconfig_frame.py b/src/pygpsclient/serialconfig_frame.py
index d1e9a89a..6ac96494 100644
--- a/src/pygpsclient/serialconfig_frame.py
+++ b/src/pygpsclient/serialconfig_frame.py
@@ -149,7 +149,7 @@ def _body(self):
self._frm_basic,
border=2,
relief="sunken",
- width=38,
+ width=30,
height=5,
justify=LEFT,
exportselection=False,
diff --git a/src/pygpsclient/serverconfig_frame.py b/src/pygpsclient/serverconfig_dialog.py
similarity index 92%
rename from src/pygpsclient/serverconfig_frame.py
rename to src/pygpsclient/serverconfig_dialog.py
index b7dec45b..e3fc8289 100644
--- a/src/pygpsclient/serverconfig_frame.py
+++ b/src/pygpsclient/serverconfig_dialog.py
@@ -1,7 +1,7 @@
"""
-serverconfig_frame.py
+serverconfig_dialog.py
-Socket Server / NTRIP caster configuration panel Frame class.
+Socket Server / NTRIP caster configuration panel Dialog class.
Supports two modes of operation - Socket Server and NTRIP Caster.
If running in NTRIP Caster mode, two base station modes are available -
@@ -20,11 +20,13 @@
# pylint: disable=unused-argument, too-many-lines
import logging
+from pathlib import Path
from time import sleep
from tkinter import (
DISABLED,
EW,
NORMAL,
+ NSEW,
Button,
Checkbutton,
DoubleVar,
@@ -41,7 +43,7 @@
from tkinter.ttk import Progressbar
from PIL import Image, ImageTk
-from pygnssutils import RTCMTYPES, check_pemfile
+from pygnssutils import RTCMTYPES
from pynmeagps import NMEAMessage, ecef2llh, llh2ecef
from pyubx2 import SET_LAYER_RAM, TXN_NONE, UBXMessage
@@ -91,6 +93,7 @@
)
from pygpsclient.strings import (
DLGNOTLS,
+ DLGTSERVER,
LBLACCURACY,
LBLCONFIGBASE,
LBLDISNMEA,
@@ -102,6 +105,7 @@
LBLSERVERPORT,
LBLSOCKSERVE,
)
+from pygpsclient.toplevel_dialog import ToplevelDialog
ACCURACIES = (
10.0,
@@ -133,34 +137,32 @@
POS_LLH = "LLH"
PQTMVER = "PQTMVER"
POSMODES = (POS_LLH, POS_ECEF)
+MINDIM = (400, 600)
-class ServerConfigFrame(Frame):
+class ServerConfigDialog(ToplevelDialog):
"""
- Server configuration frame class.
+ Server configuration dialog class.
"""
- def __init__(self, app, container, *args, **kwargs):
+ def __init__(self, app, *args, **kwargs):
"""
Constructor.
:param Frame app: reference to main tkinter application
- :param Frame container: reference to container frame
:param args: optional args to pass to Frame parent class
:param kwargs: optional kwargs for value ranges, or to pass to Frame parent class
"""
- Frame.__init__(self, container, *args, **kwargs)
+ self.__app = app
self.logger = logging.getLogger(__name__)
+ super().__init__(app, DLGTSERVER, MINDIM)
- self.__app = app
- self._container = container
self._show_advanced = False
self._socket_serve = IntVar()
self.sock_port = StringVar()
self.sock_host = StringVar()
self.sock_mode = StringVar()
- self._sock_clients = IntVar()
self.receiver_type = StringVar()
self.base_mode = StringVar()
self.https = IntVar()
@@ -184,21 +186,23 @@ def __init__(self, app, container, *args, **kwargs):
self._body()
self._do_layout()
- self.reset()
+ self._reset()
# self._attach_events() # done in reset
self._attach_events1()
+ self._finalise()
def _body(self):
"""
Set up widgets.
"""
- self._frm_basic = Frame(self)
+ self._frm_body = Frame(self.container, borderwidth=2, relief="groove")
+ self._frm_basic = Frame(self._frm_body)
self._chk_socketserve = Checkbutton(
self._frm_basic,
text=LBLSOCKSERVE,
variable=self._socket_serve,
- state=DISABLED,
+ state=NORMAL,
)
self._lbl_sockmode = Label(
self._frm_basic,
@@ -253,11 +257,6 @@ def _body(self):
relief="sunken",
width=6,
)
- self._lbl_clients = Label(self._frm_basic, text="Clients")
- self._lbl_sockclients = Label(
- self._frm_basic,
- textvariable=self._sock_clients,
- )
self._btn_toggle = Button(
self._frm_basic,
command=self._on_toggle_advanced,
@@ -266,7 +265,7 @@ def _body(self):
height=22,
# state=DISABLED,
)
- self._frm_advanced = Frame(self)
+ self._frm_advanced = Frame(self._frm_body)
self._lbl_user = Label(
self._frm_advanced,
text="User",
@@ -401,6 +400,7 @@ def _do_layout(self):
Layout widgets.
"""
+ self._frm_body.grid(column=0, row=0, sticky=NSEW)
self._frm_basic.grid(column=0, row=0, columnspan=5, sticky=EW)
self._chk_socketserve.grid(
column=0, row=0, columnspan=2, rowspan=2, padx=2, pady=1, sticky=W
@@ -409,8 +409,6 @@ def _do_layout(self):
self._spn_sockmode.grid(column=3, row=0, padx=2, pady=1, sticky=W)
self._lbl_sockhost.grid(column=0, row=2, padx=2, pady=1, sticky=W)
self._ent_sockhost.grid(column=1, row=2, padx=2, pady=1, sticky=W)
- self._lbl_clients.grid(column=2, row=2, padx=2, pady=1, sticky=W)
- self._lbl_sockclients.grid(column=3, row=2, padx=2, pady=1, sticky=W)
self._lbl_sockport.grid(column=0, row=3, padx=2, pady=1, sticky=W)
self._ent_sockport.grid(column=1, row=3, padx=2, pady=1, sticky=W)
self._chk_https.grid(column=2, row=3, columnspan=2, padx=2, pady=1, sticky=W)
@@ -426,14 +424,15 @@ def _do_layout(self):
self._lbl_basemode.grid(column=0, row=1, padx=2, pady=1, sticky=E)
self._spn_basemode.grid(column=1, row=1, padx=2, pady=1, sticky=W)
- def reset(self):
+ def _reset(self):
"""
Reset settings to defaults.
"""
self._attach_events(False)
cfg = self.__app.configuration
- self._socket_serve.set(cfg.get("sockserver_b"))
+ # self._socket_serve.set(cfg.get("sockserver_b"))
+ self._socket_serve.set(self.__app.server_status >= 0) # TODO
self.sock_mode.set(SOCKMODES[cfg.get("sockmode_b")])
self._on_toggle_advanced()
self.base_mode.set(cfg.get("ntripcasterbasemode_s"))
@@ -449,10 +448,10 @@ def reset(self):
self.disable_nmea.set(cfg.get("ntripcasterdisablenmea_b"))
self.sock_host.set(cfg.get("sockhost_s"))
https = cfg.get("sockhttps_b")
- pem, pemexists = check_pemfile()
- if https and not pemexists:
+ pem = cfg.get("tlspempath_s")
+ if https and not Path(pem).exists():
err = DLGNOTLS.format(hostpem=pem)
- self.__app.status_label = (err, ERRCOL)
+ self.status_label = (err, ERRCOL)
self.logger.error(err)
cfg.set("sockhttps_b", 0)
self._chk_https.config(state=DISABLED)
@@ -466,7 +465,6 @@ def reset(self):
self.sock_port.set(cfg.get("sockport_n"))
self.user.set(cfg.get("ntripcasteruser_s"))
self.password.set(cfg.get("ntripcasterpassword_s"))
- self.clients = 0
self._fixed_lat_temp = self.fixedlat.get()
self._fixed_lon_temp = self.fixedlon.get()
self._fixed_hae_temp = self.fixedhae.get()
@@ -558,7 +556,6 @@ def set_status(self, status: int):
if status == DISCONNECTED:
self._chk_socketserve.configure(state=DISABLED)
self._socket_serve.set(0)
- self.clients = 0
else:
self._chk_socketserve.configure(state=NORMAL)
@@ -568,9 +565,9 @@ def _on_socketserve(self, var, index, mode):
"""
if self.valid_settings():
- self.__app.status_label = ("", INFOCOL)
+ self.status_label = ("", INFOCOL)
else:
- self.__app.status_label = ("ERROR - invalid entry", ERRCOL)
+ self.status_label = ("ERROR - invalid entry", ERRCOL)
return
self._quectel_restart = 0
@@ -584,7 +581,6 @@ def _on_socketserve(self, var, index, mode):
else: # stop server
self.__app.sockserver_stop()
self.__app.stream_handler.sock_serve = False
- self.clients = 0
# set visibility of various fields depending on server status
for wid in (
@@ -650,9 +646,9 @@ def _config_receiver(self):
# validate settings
if self.valid_settings():
- self.__app.status_label = ("", INFOCOL)
+ self.status_label = ("", INFOCOL)
else:
- self.__app.status_label = ("ERROR - invalid entry", ERRCOL)
+ self.status_label = ("ERROR - invalid entry", ERRCOL)
return
delay = self.__app.configuration.get("guiupdateinterval_f") / 2
@@ -860,10 +856,10 @@ def _on_update_https(self, var, index, mode):
Action when https flag is updated.
"""
- pem, pemexists = check_pemfile()
- if self.https.get() and not pemexists:
+ pem = self.__app.configuration.get("tlspempath_s")
+ if self.https.get() and not Path(pem).exists():
err = DLGNOTLS.format(hostpem=pem)
- self.__app.status_label = (err, ERRCOL)
+ self.status_label = (err, ERRCOL)
self.logger.error(err)
self._attach_events(False)
self.https.set(0)
@@ -1000,26 +996,6 @@ def _set_coords(self, posmode: str):
self.fixedlon.set(lon)
self.fixedhae.set(hae)
- @property
- def clients(self) -> int:
- """
- Getter for number of socket clients.
- """
-
- return self._sock_clients.get()
-
- @clients.setter
- def clients(self, clients: int):
- """
- Setter for number of socket clients.
-
- :param int clients: no of clients connected
- """
-
- self._sock_clients.set(clients)
- if self._socket_serve.get() in ("1", 1):
- self.__app.frm_banner.update_transmit_status(clients)
-
def _config_msg_rates(self, rate: int, port_type: str):
"""
Configure RTCM3 and UBX NAV-SVIN message rates.
diff --git a/src/pygpsclient/settings_frame.py b/src/pygpsclient/settings_frame.py
index 1423fa26..5ce697fb 100644
--- a/src/pygpsclient/settings_frame.py
+++ b/src/pygpsclient/settings_frame.py
@@ -77,6 +77,7 @@
ICON_NTRIPCONFIG,
ICON_SERIAL,
ICON_SOCKET,
+ ICON_TRANSMIT,
ICON_TTYCONFIG,
ICON_UBXCONFIG,
INFOCOL,
@@ -93,22 +94,24 @@
UMM,
)
from pygpsclient.serialconfig_frame import SerialConfigFrame
-from pygpsclient.serverconfig_frame import ServerConfigFrame
from pygpsclient.socketconfig_frame import SocketConfigFrame
from pygpsclient.sqlite_handler import SQLOK
from pygpsclient.strings import (
DLGTNMEA,
DLGTNTRIP,
+ DLGTSERVER,
DLGTTTY,
DLGTUBX,
+ LBLAUTOSCROLL,
LBLDATABASERECORD,
LBLDATADISP,
LBLDATALOG,
LBLDEGFORMAT,
+ LBLFILEDELAY,
LBLNMEACONFIG,
LBLNTRIPCONFIG,
LBLPROTDISP,
- LBLSHOWUNUSED,
+ LBLSERVERCONFIG,
LBLTRACKRECORD,
LBLTTYCONFIG,
LBLUBXCONFIG,
@@ -116,18 +119,17 @@
MAXLINES = ("200", "500", "1000", "2000", "100")
FILEDELAYS = (2, 5, 10, 20, 50, 100, 200, 500, 1000, 2000)
-# initial dimensions adjusted for different widget
-# rendering on different platforms
+# initial dimensions (in font character units) adjusted
+# for different widget rendering on different platforms
if system() == "Linux": # Wayland
MINHEIGHT = 28
- MINWIDTH = 28
+ MINWIDTH = 24
elif system() == "Darwin": # MacOS
-
- MINHEIGHT = 38
- MINWIDTH = 30
-else: # Windows and others
- MINHEIGHT = 35
+ MINHEIGHT = 37
MINWIDTH = 26
+else: # Windows and others
+ MINHEIGHT = 34
+ MINWIDTH = 22
class SettingsFrame(Frame):
@@ -170,7 +172,6 @@ def __init__(self, app, *args, **kwargs):
self._logformat = StringVar()
self._record_track = IntVar()
self._record_database = IntVar()
- self._show_unusedsat = IntVar()
self._colortag = IntVar()
self.defaultports = self.__app.configuration.get("defaultport_s")
self._validsettings = True
@@ -183,6 +184,7 @@ def __init__(self, app, *args, **kwargs):
self._img_nmeaconfig = ImageTk.PhotoImage(Image.open(ICON_NMEACONFIG))
self._img_ttyconfig = ImageTk.PhotoImage(Image.open(ICON_TTYCONFIG))
self._img_ntripconfig = ImageTk.PhotoImage(Image.open(ICON_NTRIPCONFIG))
+ self._img_serverconfig = ImageTk.PhotoImage(Image.open(ICON_TRANSMIT))
self._img_dataread = ImageTk.PhotoImage(Image.open(ICON_LOGREAD))
self._container() # create scrollable container
@@ -382,7 +384,7 @@ def _body(self):
textvariable=self._units,
)
self._chk_scroll = Checkbutton(
- self._frm_options, text="Autoscroll", variable=self._autoscroll
+ self._frm_options, text=LBLAUTOSCROLL, variable=self._autoscroll
)
self._spn_maxlines = Spinbox(
self._frm_options,
@@ -394,7 +396,7 @@ def _body(self):
)
self._lbl_filedelay = Label(
self._frm_options,
- text="File Delay",
+ text=LBLFILEDELAY,
)
self._spn_filedelay = Spinbox(
self._frm_options,
@@ -406,9 +408,6 @@ def _body(self):
repeatdelay=1000,
repeatinterval=1000,
)
- self._chk_unusedsat = Checkbutton(
- self._frm_options, text=LBLSHOWUNUSED, variable=self._show_unusedsat
- )
self._chk_datalog = Checkbutton(
self._frm_options,
text=LBLDATALOG,
@@ -476,10 +475,16 @@ def _body(self):
command=lambda: self.__app.start_dialog(DLGTNTRIP),
state=NORMAL,
)
- # socket server configuration
- self.frm_socketserver = ServerConfigFrame(
- self.__app,
- self._frm_container,
+ self._lbl_serverconfig = Label(
+ self._frm_options_btns,
+ text=LBLSERVERCONFIG,
+ )
+ self._btn_serverconfig = Button(
+ self._frm_options_btns,
+ width=45,
+ image=self._img_serverconfig,
+ command=lambda: self.__app.start_dialog(DLGTSERVER),
+ state=NORMAL,
)
def _do_layout(self):
@@ -518,49 +523,42 @@ def _do_layout(self):
self._lbl_protocol.grid(column=0, row=0, padx=2, pady=2, sticky=W)
self._chk_nmea.grid(column=1, row=0, padx=0, pady=0, sticky=W)
self._chk_ubx.grid(column=2, row=0, padx=0, pady=0, sticky=W)
- self._chk_sbf.grid(column=3, row=0, padx=0, pady=0, sticky=W)
- self._chk_qgc.grid(column=4, row=0, padx=0, pady=0, sticky=W)
- self._chk_rtcm.grid(column=1, row=1, padx=0, pady=0, sticky=W)
- self._chk_spartn.grid(column=2, row=1, padx=0, pady=0, sticky=W)
- self._chk_tty.grid(column=3, row=1, padx=0, pady=0, sticky=W)
- self._lbl_consoledisplay.grid(column=0, row=2, padx=2, pady=2, sticky=W)
+ self._chk_rtcm.grid(column=3, row=0, padx=0, pady=0, sticky=W)
+ self._chk_sbf.grid(column=1, row=1, padx=0, pady=0, sticky=W)
+ 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._lbl_consoledisplay.grid(column=0, row=3, padx=2, pady=2, sticky=W)
self._spn_conformat.grid(
- column=1, row=2, columnspan=2, padx=1, pady=2, sticky=W
- )
- self._chk_tags.grid(column=3, row=2, padx=1, pady=2, sticky=W)
- self._lbl_format.grid(column=0, row=3, padx=2, pady=2, sticky=W)
- self._spn_format.grid(column=1, row=3, padx=2, pady=2, sticky=W)
- self._spn_units.grid(column=2, row=3, columnspan=2, padx=2, pady=2, sticky=W)
- self._chk_scroll.grid(column=0, row=5, padx=2, pady=2, sticky=W)
- self._spn_maxlines.grid(column=1, row=5, padx=2, pady=2, sticky=W)
- self._lbl_filedelay.grid(column=2, row=5, padx=2, pady=2, sticky=E)
- self._spn_filedelay.grid(column=3, row=5, padx=2, pady=2, sticky=W)
- self._chk_unusedsat.grid(
- column=0, row=6, columnspan=2, padx=2, pady=2, sticky=W
- )
- self._chk_datalog.grid(column=0, row=7, padx=2, pady=2, sticky=W)
- self._spn_datalog.grid(column=1, row=7, columnspan=3, padx=2, pady=2, sticky=W)
+ column=1, row=3, columnspan=2, padx=1, pady=2, sticky=W
+ )
+ self._chk_tags.grid(column=3, row=3, padx=1, pady=2, sticky=W)
+ self._lbl_format.grid(column=0, row=4, padx=2, pady=2, sticky=W)
+ self._spn_format.grid(column=1, row=4, padx=2, pady=2, sticky=W)
+ self._spn_units.grid(column=2, row=4, columnspan=2, padx=2, pady=2, sticky=W)
+ self._chk_scroll.grid(column=0, row=6, padx=2, pady=2, sticky=W)
+ self._spn_maxlines.grid(column=1, row=6, padx=2, pady=2, sticky=W)
+ self._lbl_filedelay.grid(column=2, row=6, padx=2, pady=2, sticky=E)
+ self._spn_filedelay.grid(column=3, row=6, padx=2, pady=2, sticky=W)
+ self._chk_datalog.grid(column=0, row=8, padx=2, pady=2, sticky=W)
+ self._spn_datalog.grid(column=1, row=8, columnspan=3, padx=2, pady=2, sticky=W)
self._chk_recordtrack.grid(
- column=0, row=8, columnspan=2, padx=2, pady=2, sticky=W
+ column=0, row=9, columnspan=2, padx=2, pady=2, sticky=W
)
self._chk_recorddatabase.grid(
- column=2, row=8, columnspan=2, padx=2, pady=2, sticky=W
+ column=2, row=9, columnspan=2, padx=2, pady=2, sticky=W
)
- self._frm_options_btns.grid(column=0, row=9, columnspan=4, sticky=EW)
- self._btn_ubxconfig.grid(column=0, row=0, padx=5)
+ self._frm_options_btns.grid(column=0, row=10, columnspan=4, sticky=EW)
+ self._btn_ubxconfig.grid(column=0, row=0, padx=2, pady=1)
self._lbl_ubxconfig.grid(column=0, row=1)
- self._btn_nmeaconfig.grid(column=1, row=0, padx=5)
+ self._btn_nmeaconfig.grid(column=1, row=0, padx=2, pady=1)
self._lbl_nmeaconfig.grid(column=1, row=1)
- self._btn_ttyconfig.grid(column=2, row=0, padx=5)
+ self._btn_ttyconfig.grid(column=2, row=0, padx=2, pady=1)
self._lbl_ttyconfig.grid(column=2, row=1)
- self._btn_ntripconfig.grid(column=3, row=0, padx=5)
+ self._btn_ntripconfig.grid(column=3, row=0, padx=2, pady=1)
self._lbl_ntripconfig.grid(column=3, row=1)
- ttk.Separator(self._frm_container).grid(
- column=0, row=10, columnspan=4, padx=2, pady=2, sticky=EW
- )
- self.frm_socketserver.grid(
- column=0, row=11, columnspan=4, padx=2, pady=2, sticky=EW
- )
+ self._btn_serverconfig.grid(column=4, row=0, padx=2, pady=1)
+ self._lbl_serverconfig.grid(column=4, row=1)
def _attach_events(self, add: bool = True):
"""
@@ -587,7 +585,6 @@ def _attach_events(self, add: bool = True):
self._units.trace_update(tracemode, self._on_update_units, add)
self._degrees_format.trace_update(tracemode, self._on_update_degreesformat, add)
self._console_format.trace_update(tracemode, self._on_update_consoleformat, add)
- self._show_unusedsat.trace_update(tracemode, self._on_update_unusedsat, add)
self._colortag.trace_update(tracemode, self._on_update_colortag, add)
self._logformat.trace_update(tracemode, self._on_update_logformat, add)
self._datalog.trace_update(tracemode, self._on_data_log, add)
@@ -615,7 +612,6 @@ def reset(self):
self._maxlines.set(cfg.get("maxlines_n"))
self._filedelay.set(cfg.get("filedelay_n"))
self._console_format.set(cfg.get("consoleformat_s"))
- self._show_unusedsat.set(cfg.get("unusedsat_b"))
self._logformat.set(cfg.get("logformat_s"))
self._datalog.set(cfg.get("datalog_b"))
self.logpath = cfg.get("logpath_s")
@@ -768,13 +764,6 @@ def _on_update_autoscroll(self, var, index, mode):
self.__app.configuration.set("autoscroll_b", self._autoscroll.get())
- def _on_update_unusedsat(self, var, index, mode):
- """
- Action on updating unused satellites.
- """
-
- self.__app.configuration.set("unusedsat_b", self._show_unusedsat.get())
-
def _on_update_logformat(self, var, index, mode):
"""
Action on updating log format.
@@ -874,9 +863,10 @@ def _on_connect(self, conntype: int):
"conntype": conntype,
"msgmode": self.frm_serial.msgmode,
"inactivity_timeout": self.frm_serial.inactivity_timeout,
+ "tlscrtpath": self.__app.configuration.get("tlscrtpath_s"),
}
- self.frm_socketserver.status_label = conntype
+ # self.frm_socketserver.status_label = conntype
if conntype == CONNECTED:
frm = self.frm_serial
if frm.status == NOPORTS:
@@ -935,7 +925,6 @@ def enable_controls(self, status: int):
self.frm_serial.status_label = status
self.frm_socketclient.status_label = status
- self.frm_socketserver.status_label = status
self._btn_connect.config(
state=(
diff --git a/src/pygpsclient/signalsview_frame.py b/src/pygpsclient/signalsview_frame.py
new file mode 100644
index 00000000..cba0f641
--- /dev/null
+++ b/src/pygpsclient/signalsview_frame.py
@@ -0,0 +1,370 @@
+"""
+signalsview_frame.py
+
+Signals view frame class for PyGPSClient application.
+
+This handles a frame containing a graph of current signal C/No level,
+correction source and other signal-related flags.
+
+Created on 24 Dec 2025
+
+:author: semuadmin (Steve Smith)
+:copyright: 2020 semuadmin
+:license: BSD 3-Clause
+"""
+
+# pylint: disable=no-member, unused-variable, duplicate-code
+
+from tkinter import ALL, NSEW, SE, Frame, N, S, W, font
+
+from pyubx2 import CORRSOURCE, SIGID, UBXMessage
+
+from pygpsclient.canvas_plot import (
+ TAG_DATA,
+ TAG_GRID,
+ TAG_XLABEL,
+ TAG_YLABEL,
+ CanvasGraph,
+)
+from pygpsclient.globals import (
+ BGCOL,
+ FGCOL,
+ GNSS_LIST,
+ GRIDMAJCOL,
+ MAX_SNR,
+ PNTCOL,
+ SIGNALSVIEW,
+ WIDGETU3,
+)
+from pygpsclient.helpers import col2contrast, fitfont, setubxrate
+from pygpsclient.strings import DLGENABLENAVSIG, DLGNONAVSIG, DLGWAITNAVSIG
+
+OL_WID = 1
+FONTSCALELG = 40
+MAXWAIT = 10
+ACTIVE = ""
+XLBLANGLE = 60
+XLBLFMT = "000 WWW_W/W"
+# Correction source legend
+CSLEG = ", ".join(
+ f"{key} {val}" for key, val in CORRSOURCE.items() if key != 0
+).replace(", 7", ",\n7")
+
+
+def unused_sigs(data: dict) -> int:
+ """
+ Get number of 'unused' sigs in gnss_data.sig_data.
+
+ :param dict data: sig_data
+ :return: number of sigs where cno = 0
+ :rtype: int
+ """
+
+ return sum(1 for (_, _, _, cno, _, _, _, _) in data.values() if cno == 0)
+
+
+class SignalsviewFrame(Frame):
+ """
+ Signalsview frame class.
+ """
+
+ def __init__(self, app, *args, **kwargs):
+ """
+ Constructor.
+
+ :param Frame app: reference to main tkinter application
+ :param args: optional args to pass to Frame parent class
+ :param kwargs: optional kwargs to pass to Frame parent class
+ """
+
+ self.__app = app # Reference to main application class
+ self.__master = self.__app.appmaster # Reference to root class (Tk)
+
+ Frame.__init__(self, self.__master, *args, **kwargs)
+
+ def_w, def_h = WIDGETU3
+ 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._body()
+ self._attach_events()
+
+ def _body(self):
+ """
+ Set up frame and widgets.
+ """
+
+ self.grid_columnconfigure(0, weight=1)
+ self.grid_rowconfigure(0, weight=1)
+ self._canvas = CanvasGraph(
+ self.__app, self, width=self.width, height=self.height, bg=BGCOL
+ )
+ self._canvas.grid(column=0, row=0, sticky=NSEW)
+
+ def _attach_events(self):
+ """
+ Bind events to frame.
+ """
+
+ self.bind("", self._on_resize)
+ self._canvas.bind("", self._on_legend)
+ self._canvas.bind("", self._on_cno0)
+ self._canvas.bind("", self._on_cno0)
+
+ def _on_legend(self, event): # pylint: disable=unused-argument
+ """
+ On double-click - toggle legend on/off.
+
+ :param event: event
+ """
+
+ self.__app.configuration.set(
+ "legend_b", not self.__app.configuration.get("legend_b")
+ )
+ self._redraw = True
+
+ def _on_cno0(self, event): # pylint: disable=unused-argument
+ """
+ On double-right-click - include signals where C/No = 0.
+
+ :param event: event
+ """
+
+ self.__app.configuration.set(
+ "unusedsat_b", not self.__app.configuration.get("unusedsat_b")
+ )
+ self._redraw = True
+
+ def enable_messages(self, status: bool):
+ """
+ Enable/disable UBX NAV-SIG message.
+
+ :param bool status: 0 = off, 1 = on
+ """
+
+ 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):
+ """
+ Set pending confirmation flag for Signalsview frame to
+ signify that it's waiting for a confirmation message.
+
+ :param int msgid: UBX message identity
+ :param int ubxfrm: integer representing UBX configuration frame
+ """
+
+ self._pending_confs[msgid] = ubxfrm
+
+ def update_pending(self, msg: UBXMessage):
+ """
+ Receives polled confirmation message from the ubx_handler and
+ updates signalsview canvas.
+
+ :param UBXMessage msg: UBX config message
+ """
+
+ pending = self._pending_confs.get(msg.identity, False)
+
+ if pending and msg.identity == "ACK-NAK":
+ self.reset()
+ w, h = self.width, self.height
+ self._canvas.create_text(
+ w / 2,
+ h / 2,
+ text=DLGNONAVSIG,
+ fill=PNTCOL,
+ anchor=S,
+ 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")
+
+ def reset(self):
+ """
+ Reset spectrumview frame.
+ """
+
+ self.__app.gnss_status.sig_data = []
+ self._canvas.delete(ALL)
+ self.update_frame()
+
+ def init_frame(self):
+ """
+ Initialise graph view
+ """
+
+ # only redraw the tags that have changed
+ tags = (TAG_GRID, TAG_XLABEL, TAG_YLABEL) if self._redraw else ()
+ self._canvas.create_graph(
+ xdatamax=10,
+ ydatamax=(MAX_SNR,),
+ xtickmaj=10,
+ ytickmaj=int(MAX_SNR / 10),
+ ylegend=("C/No dBHz",),
+ ycol=(FGCOL,),
+ ylabels=True,
+ xlabelsfrm=XLBLFMT,
+ xangle=XLBLANGLE,
+ fontscale=FONTSCALELG,
+ tags=tags,
+ )
+ 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
+ """
+
+ w = self.width / 12 / 2
+ h = self.height / 18
+
+ # gnssid color code legend
+ lgfont = font.Font(size=int(min(self.width / 2, self.height) / FONTSCALELG))
+ for i, (_, (gnssName, gnssCol)) in enumerate(GNSS_LIST.items()):
+ x = (self._canvas.xoffl * 2) + w * i
+ self._canvas.create_rectangle(
+ x,
+ self._canvas.yofft,
+ x + w - 5,
+ self._canvas.yofft + h,
+ outline=GRIDMAJCOL,
+ fill=gnssCol,
+ width=OL_WID,
+ tags=TAG_XLABEL,
+ )
+ self._canvas.create_text(
+ (x + x + w - 5) / 2,
+ self._canvas.yofft + h / 2,
+ text=gnssName,
+ fill=col2contrast(gnssCol),
+ font=lgfont,
+ tags=TAG_XLABEL,
+ )
+
+ # correction source legend
+ self._canvas.create_text(
+ self.width / 2,
+ self._canvas.yofft + h / 2,
+ text=f"Correction Source:\n{CSLEG}",
+ fill=FGCOL,
+ font=lgfont,
+ anchor=W,
+ tags=TAG_DATA,
+ )
+
+ def update_frame(self):
+ """
+ Plot signal signal-to-noise ratio (C/No).
+ Automatically adjust y axis according to number of satellites in view.
+ """
+
+ data = self.__app.gnss_status.sig_data
+ if len(data) == 0:
+ if self._waits >= MAXWAIT:
+ self._navsig_status = DLGNONAVSIG
+ else:
+ self._waits += 1
+ else:
+ 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)
+ if siv <= 0:
+ return
+
+ 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 * 1.66,
+ self._canvas.yoffb,
+ XLBLANGLE,
+ )
+ for val in sorted(data.values()): # sort by ascending gnssid, svid, sigid
+ gnssId, prn, sigid, cno, corrsource, quality, flags, _ = val
+ if cno == 0 and not show_unused:
+ continue
+ sig = SIGID.get((gnssId, sigid), sigid)
+ snr_y = int(cno) * (h - self._canvas.yoffb - 1) / MAX_SNR
+ (_, ol_col) = GNSS_LIST[gnssId]
+ prn = f"{int(prn):02}"
+ self._canvas.create_rectangle(
+ offset,
+ h - self._canvas.yoffb - 1,
+ offset + colwidth - OL_WID,
+ h - self._canvas.yoffb - snr_y - 1,
+ outline=GRIDMAJCOL,
+ fill=ol_col,
+ width=OL_WID,
+ tags=TAG_DATA,
+ )
+ # xlabel prn - sigid
+ self._canvas.create_text(
+ offset + colwidth,
+ h - self._canvas.yoffb + 2,
+ text=f"{prn} {sig}",
+ fill=FGCOL,
+ font=xfnt,
+ angle=XLBLANGLE,
+ anchor=SE,
+ tags=TAG_DATA,
+ )
+ # xcaption corrsource if > 0
+ if corrsource:
+ self._canvas.create_text(
+ offset + colwidth / 2,
+ h - self._canvas.yoffb - snr_y + 2,
+ text=corrsource,
+ fill=col2contrast(ol_col),
+ font=xfnt,
+ anchor=N,
+ tags=TAG_DATA,
+ )
+ offset += colwidth
+
+ if self.__app.configuration.get("legend_b"):
+ self._draw_legend()
+ self.update_idletasks()
+
+ def _on_resize(self, event): # pylint: disable=unused-argument
+ """
+ Resize frame
+
+ :param event event: resize event
+ """
+
+ self.width, self.height = self.get_size()
+ self._redraw = True
+
+ def get_size(self):
+ """
+ Get current canvas size.
+
+ :return: window size (width, height)
+ :rtype: tuple
+ """
+
+ self.update_idletasks() # Make sure we know about any resizing
+ return self._canvas.winfo_width(), self._canvas.winfo_height()
diff --git a/src/pygpsclient/spectrum_frame.py b/src/pygpsclient/spectrum_frame.py
index 60196b66..4156a53f 100644
--- a/src/pygpsclient/spectrum_frame.py
+++ b/src/pygpsclient/spectrum_frame.py
@@ -16,7 +16,8 @@
# pylint: disable=no-member, unused-argument
import logging
-from tkinter import ALL, EW, NSEW, NW, Checkbutton, Frame, IntVar, S, W
+from tkinter import ALL, EW, NSEW, NW, Checkbutton, Frame, IntVar, N, S, W
+from types import NoneType
from pyubx2 import UBXMessage
@@ -45,18 +46,21 @@
MAX_DB = 200
MIN_HZ = 1.1e9
MAX_HZ = 1.70e9
-RF_BANDS = {
- "B1": 1575420000,
+RF_FREQS = {
+ # "INM": 1536000000,
"B3": 1268520000,
- "B2": 1202025000,
+ "B2I": 1207140000,
"B2a": 1176450000,
+ "B1C": 1575420000,
+ "B1I": 1561098000,
"E6": 1278750000,
- "E5b": 1202025000,
+ "E5b": 1207140000,
"E5a": 1176450000,
"E1": 1575420000,
- "G3": 1202025000,
- "G2": 1248060000,
- "G1": 1600995000,
+ "G3": 1207140000,
+ "G2": 1246000000,
+ "G1": 1602000000,
+ "L6": 1278750000,
"L5": 1176450000,
"L2": 1227600000,
"L1": 1575420000,
@@ -288,7 +292,9 @@ def init_frame(self):
tags=TAG_DATA,
)
- def _update_plot(self, rfblocks: list, mode: str = MODELIVE, colors: dict = None):
+ def _update_plot(
+ self, rfblocks: list, mode: str = MODELIVE, colors: dict | NoneType = None
+ ):
"""
Update spectrum plot with live or snapshot rf block data.
@@ -307,7 +313,7 @@ def _update_plot(self, rfblocks: list, mode: str = MODELIVE, colors: dict = None
self.init_frame()
# plot frequency bands
if self._showrf:
- self._plot_rf_bands(mode)
+ self._plot_RF_FREQS(mode)
# for each RF block in MON-SPAN message
for i, rfblock in enumerate(specxy):
@@ -356,7 +362,7 @@ def _plot_rf_legend(self, col: str, mode: str, rf: int, index: int):
- rfw * (index + 1)
- (index * self._canvas.fnth)
)
- y = self._canvas.yofft * 2
+ y = 3 # self._canvas.yofft * 2
x2 = x1 + rfw
if mode == MODESNAP:
y += self._canvas.fnth
@@ -367,42 +373,40 @@ def _plot_rf_legend(self, col: str, mode: str, rf: int, index: int):
text=f"RF {rf + 1}",
fill=FGCOL,
font=self._canvas.font,
- anchor=S,
+ anchor=N,
tags=(mode, TAG_XLABEL),
)
self._canvas.create_line(
x1,
- y,
+ y + self._canvas.fnth,
x2,
- y,
+ y + self._canvas.fnth,
fill=col,
width=OL_WID,
tags=(mode, TAG_XLABEL),
)
- def _plot_rf_bands(self, mode: str):
+ def _plot_RF_FREQS(self, mode: str):
"""
- Plot RF band markers
+ Plot RF frequency markers
:param int mode: plot or snapshot
"""
- for nam, frq in RF_BANDS.items():
+ for nam, frq in RF_FREQS.items():
if self._minhz < frq < self._maxhz:
yoff, col = {
"L": (self._canvas.fnth, GNSS_LIST[0][1]), # GPS
"G": (self._canvas.fnth * 2, GNSS_LIST[6][1]), # GLONASS
"E": (self._canvas.fnth * 3, GNSS_LIST[2][1]), # Galileo
- "S": (self._canvas.fnth * 3, GNSS_LIST[2][1]), # Galileo SAR
"B": (self._canvas.fnth * 4, GNSS_LIST[3][1]), # Beidou
+ "I": (self._canvas.fnth * 5, "#FF83FA"), # INMARSAT
}[nam[0:1]]
if nam not in (
- "E1",
- "E5a",
"E5b",
- "B2a",
- "B2",
- "B1",
+ "E1",
+ "B2I",
+ "B1C",
): # same freq as other bands
self._canvas.create_gline(
frq / GHZ,
diff --git a/src/pygpsclient/stream_handler.py b/src/pygpsclient/stream_handler.py
index eb4dec2d..385536ed 100644
--- a/src/pygpsclient/stream_handler.py
+++ b/src/pygpsclient/stream_handler.py
@@ -53,7 +53,6 @@ class to read and parse incoming data from the receiver. It places
UBX_PROTOCOL,
GNSSError,
GNSSReader,
- check_pemfile,
)
from pynmeagps import NMEAMessageError, NMEAParseError, NMEAStreamError
from pyqgc import QGCMessageError, QGCParseError, QGCStreamError
@@ -220,9 +219,9 @@ def _read_thread(
context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
context.load_verify_locations(findcacerts())
if selfsign:
- pem, _ = check_pemfile()
+ crt = settings.get("tlscrtpath")
# context.verify_mode = ssl.CERT_NONE
- context.load_verify_locations(pem)
+ context.load_verify_locations(crt)
context.check_hostname = False
stream = context.wrap_socket(stream, server_hostname=server)
stream.connect(conn)
@@ -263,6 +262,7 @@ def _read_thread(
master.event_generate(settings["timeout_event"])
except (
IOError,
+ FileNotFoundError,
SerialException,
SerialTimeoutException,
OSError,
@@ -270,10 +270,15 @@ def _read_thread(
gaierror,
) as err:
if not stopevent.is_set():
+ fnam = (
+ settings.get("tlscrtpath")
+ if isinstance(err, FileNotFoundError)
+ else ""
+ )
stopevent.set()
master.event_generate(settings["error_event"])
# use after(0) to avoid tkinter main thread contention
- status.after(0, status.config, {"text": str(err), "fg": ERRCOL})
+ status.after(0, status.config, {"text": f"{err} {fnam}", "fg": ERRCOL})
def _readloop(
self,
diff --git a/src/pygpsclient/strings.py b/src/pygpsclient/strings.py
index 26ecb299..e82d7370 100644
--- a/src/pygpsclient/strings.py
+++ b/src/pygpsclient/strings.py
@@ -42,27 +42,29 @@
CONFIRM = "CONFIRM"
ENDOFFILE = "End of file reached"
FILEOPENERROR = "Error opening file {}"
+HALTTAGWARN = "HALTED ON USER TAG MATCH: {}"
INACTIVE_TIMEOUT = "Inactivity timeout"
KILLSWITCH = "Running threads terminated by user"
-LOADCONFIGNK = "Unrecognised configuration setting '{}: {}'"
LOADCONFIGBAD = "Configuration not loaded {} {}. Using defaults"
+LOADCONFIGNK = "Unrecognised configuration setting '{}: {}'"
+LOADCONFIGNONE = "Configuration file not found {}. Using defaults"
LOADCONFIGOK = "Configuration loaded {}{}"
LOADCONFIGRESAVE = ". Consider re-saving"
-LOADCONFIGNONE = "Configuration file not found {}. Using defaults"
MAPCONFIGERR = "Custom map configuration error"
MAPOPENERR = "Unable to open custom map:\n{}"
MQTTCONN = "Connecting to MQTT server {}..."
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."
NOWEBMAPHTTP = NOWEBMAP + "\nBad HTTP response: {}.\nCheck MQAPIKEY.\n"
NOWEBMAPKEY = NOWEBMAP + f"\nMQAPIKEY not found or invalid.\n\n{MAPAPI_URL}"
NULLSEND = "Nothing to send"
-OUTOFBOUNDS = "No custom map available for {}"
OPENFILEERROR = "ERROR! File could not be opened"
+OUTOFBOUNDS = "No custom map available for {}"
READTITLE = "Select File"
SAVECONFIGBAD = "Configuration not saved {}"
SAVECONFIGOK = "Configuration saved OK"
@@ -76,8 +78,6 @@
VERCHECK = f"Newer version of {TITLE} available:"
WAITNMEADATA = "Waiting for data..."
WAITUBXDATA = "Waiting for data..."
-NOWDGSWARN = "WARNING! No widgets are enabled in config file {} - display will be blank"
-HALTTAGWARN = "HALTED ON USER TAG MATCH: {}"
# Menu text
MENUABOUT = "About"
@@ -101,6 +101,7 @@
# Label text
LBLACCURACY = "Accuracy (cm)"
+LBLAUTOSCROLL = "Autoscroll"
LBLCFGGENERIC = "UBX Legacy Command Configuration"
LBLCFGGENERICNMEA = "NMEA Command Configuration"
LBLCFGMSG = "CFG-MSG Message Rate Configuration"
@@ -109,19 +110,21 @@
LBLCFGRECORD = "CFG Configuration Load/Save/Record"
LBLCONFIGBASE = "Configure Base"
LBLCTL = "Controls"
+LBLDATABASERECORD = "Database"
LBLDATADISP = "Format"
LBLDATALOG = "Datalog"
-LBLDATABASERECORD = "Database"
LBLDEGFORMAT = "Units"
+LBLDISNMEA = "Disable NMEA"
LBLDURATIONS = "Duration (s)"
+LBLFILEDELAY = "File Read Delay"
LBLGGAFIXED = "Fixed Reference"
LBLGGALIVE = "Receiver"
LBLJSONLOAD = "Load Keys From JSON"
LBLLANIP = "LAN IP"
-LBLSHOWTRACK = "Track"
+LBLNMEACONFIG = "NMEA\nConfig"
+LBLNMEAPRESET = "Preset NMEA Configuration Commands"
LBLNODATA = "No data available"
-LBLNMEACONFIG = "NMEA Config"
-LBLNTRIPCONFIG = "NTRIP Client"
+LBLNTRIPCONFIG = "NTRIP\nClient"
LBLNTRIPGGAINT = "GGA Interval s"
LBLNTRIPMOUNT = "Mountpoint"
LBLNTRIPPORT = "Port"
@@ -130,20 +133,20 @@
LBLNTRIPSTR = "Sourcetable"
LBLNTRIPUSER = "User"
LBLNTRIPVERSION = "Version"
-LBLNMEAPRESET = "Preset NMEA Configuration Commands"
-LBLUBXPRESET = "Preset UBX Configuration Commands"
LBLPROTDISP = "Protocols"
LBLPUBLICIP = "Public IP"
+LBLSERVERCONFIG = "Server\nConfig"
LBLSERVERHOST = "Host IP"
LBLSERVERMODE = "Mode"
LBLSERVERPORT = "Port"
LBLSET = "Settings"
-LBLSHOWUNUSED = "Include C/No = 0"
+LBLSHOWTRACK = "Track"
LBLSOCKSERVE = "Socket Server /\nNTRIP Caster " # padded to align
LBLSPARTNCONFIG = "SPARTN Client"
LBLSPARTNGN = "GNSS RECEIVER CONFIGURATION (F9*)"
LBLSPARTNIP = "IP CORRECTION CONFIGURATION (MQTT)"
LBLSPARTNLB = "L-BAND CORRECTION CONFIGURATION (D9*)"
+LBLSPORT = "SAVED PORT"
LBLSPTNCURR = "CURRENT SPARTN KEY:"
LBLSPTNDAT = "Valid from YYYYMMDD"
LBLSPTNFP = "Configure receiver"
@@ -152,16 +155,16 @@
LBLSPTNUPLOAD = "Upload keys"
LBLSTREAM = "Stream\nfrom file"
LBLTRACKRECORD = "GPX Track"
-LBLTTYCONFIG = "TTY Config"
-LBLUBXCONFIG = "UBX Config"
+LBLTTYCONFIG = "TTY\nConfig"
+LBLUBXCONFIG = "UBX\nConfig"
+LBLUBXPRESET = "Preset UBX Configuration Commands"
LBLUDPORT = "USER-DEFINED PORT"
-LBLSPORT = "SAVED PORT"
-LBLDISNMEA = "Disable NMEA"
# 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"
DLGGPXERROR = "GPX Parsing Error!"
DLGGPXOPEN = "Click folder icon to open GPX Track file"
DLGGPXLOAD = "Loading GPX file ..."
@@ -175,19 +178,22 @@
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?"
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"
DLGTNTRIP = "NTRIP Configuration"
+DLGTSERVER = "Server Configuration"
DLGTRECORD = "Configuration Command Recorder"
DLGTSPARTN = "SPARTN Configuration"
DLGTNMEA = "NMEA Configuration"
DLGTUBX = "UBX Configuration"
DLGTIMPORTMAP = "Import Custom Map"
DLGTTTY = "TTY Commands"
-DLGNOTLS = "TLS certificate '{hostpem}' not found"
+DLGNOTLS = "TLS PEM file '{hostpem}' not found"
diff --git a/src/pygpsclient/ubx_handler.py b/src/pygpsclient/ubx_handler.py
index 792438d9..a06bbd05 100644
--- a/src/pygpsclient/ubx_handler.py
+++ b/src/pygpsclient/ubx_handler.py
@@ -22,8 +22,8 @@
from pygpsclient.globals import GLONASS_NMEA, UTF8
from pygpsclient.helpers import corrage2int, fix2desc, ned2vector, svid2gnssid
-from pygpsclient.strings import DLGTSPARTN, DLGTUBX, NA
-from pygpsclient.widget_state import VISIBLE, WDGSPECTRUM, WDGSYSMON
+from pygpsclient.strings import DLGTSERVER, DLGTSPARTN, DLGTUBX, NA
+from pygpsclient.widget_state import VISIBLE, WDGSIGNALS, WDGSPECTRUM, WDGSYSMON
class UBXHandler:
@@ -79,6 +79,8 @@ def process_data(self, raw_data: bytes, parsed_data: object):
self._process_NAV_RELPOSNED(parsed_data)
elif parsed_data.identity in ("NAV-SAT", "NAV2-SAT"):
self._process_NAV_SAT(parsed_data)
+ elif parsed_data.identity in ("NAV-SIG", "NAV2-SIG"):
+ self._process_NAV_SIG(parsed_data)
elif parsed_data.identity in ("NAV-STATUS", "NAV2-STATUS"):
self._process_NAV_STATUS(parsed_data)
elif parsed_data.identity == "NAV-SVIN":
@@ -118,10 +120,10 @@ def _process_ACK(self, msg: UBXMessage):
if self.__app.dialog(DLGTSPARTN) is not None:
self.__app.dialog(DLGTSPARTN).update_pending(msg)
- # if Spectrumview or Sysmon widgets are active, send ACKSs there
+ # if Spectrumview, Sysmon or Signals widgets are active, send ACKSs there
if msg.identity in ("ACK-ACK", "ACK-NAK"):
wdgs = self.__app.widget_state.state
- for wdg in (WDGSYSMON, WDGSPECTRUM):
+ for wdg in (WDGSYSMON, WDGSPECTRUM, WDGSIGNALS):
if wdgs[wdg][VISIBLE]:
if msg.clsID == 6 and msg.msgID == 1: # CFG-MSG
getattr(self.__app, wdgs[wdg]["frm"]).update_pending(msg)
@@ -347,6 +349,47 @@ def _process_NAV_SAT(self, data: UBXMessage):
self.__app.gnss_status.siv = len(self.__app.gnss_status.gsv_data)
+ def _process_NAV_SIG(self, data: UBXMessage):
+ """
+ Process NAV-SIG sentences - Signal Information.
+
+ NB: For consistency with NMEA GSV and UBX NAV-SVINFO message types,
+ this uses the NMEA SVID numbering range for GLONASS satellites
+ (65 - 96) rather than the Slot ID (1-24) by default.
+ To change this, set the GLONASS_NMEA flag in globals.py to False.
+
+ :param UBXMessage data: NAV-SIG parsed message
+ """
+
+ self.__app.gnss_status.sig_data = {}
+ num_sig = int(data.numSigs)
+ now = time()
+
+ for i in range(num_sig):
+ idx = f"_{i+1:02d}"
+ gnssId = getattr(data, "gnssId" + idx)
+ svid = getattr(data, "svId" + idx)
+ sigid = getattr(data, "sigId" + idx)
+ # use NMEA GLONASS numbering (65-96) rather than slotID (1-24)
+ if gnssId == 6 and svid < 25 and svid != 255 and GLONASS_NMEA:
+ svid += 64
+ cno = getattr(data, "cno" + idx)
+ corrsource = getattr(data, "corrSource" + idx)
+ quality = getattr(data, "qualityInd" + idx)
+ sigflags = 0 # TODO collate sigFlags bits
+ self.__app.gnss_status.sig_data[(gnssId, svid, sigid)] = (
+ gnssId,
+ svid,
+ sigid,
+ cno,
+ corrsource,
+ quality,
+ sigflags,
+ now,
+ )
+
+ # print(f"DEBUG {self.__app.gnss_status.sig_data=}")
+
def _process_NAV_STATUS(self, data: UBXMessage):
"""
Process NAV-STATUS sentences - Status Information.
@@ -366,8 +409,8 @@ def _process_NAV_SVIN(self, data: UBXMessage):
:param UBXMessage data: NAV-SVIN parsed message
"""
- if self.__app.frm_settings.frm_socketserver is not None:
- self.__app.frm_settings.frm_socketserver.svin_countdown(
+ if self.__app.dialog(DLGTSERVER) is not None:
+ self.__app.dialog(DLGTSERVER).svin_countdown(
data.dur, data.valid, data.active
)
diff --git a/src/pygpsclient/widget_state.py b/src/pygpsclient/widget_state.py
index 2d3ce23e..b2cc429f 100644
--- a/src/pygpsclient/widget_state.py
+++ b/src/pygpsclient/widget_state.py
@@ -34,6 +34,7 @@ class definition and update `ubx_handler` to populate them.
from pygpsclient.rover_frame import RoverFrame
from pygpsclient.scatter_frame import ScatterViewFrame
from pygpsclient.settings_frame import SettingsFrame
+from pygpsclient.signalsview_frame import SignalsviewFrame
from pygpsclient.skyview_frame import SkyviewFrame
from pygpsclient.spectrum_frame import SpectrumviewFrame
from pygpsclient.status_frame import StatusFrame
@@ -66,6 +67,7 @@ class definition and update `ubx_handler` to populate them.
WDGSYSMON = "System Monitor"
WDGCHART = "Chart Plot"
WDGIMUMON = "IMU Monitor"
+WDGSIGNALS = "Signals"
class WidgetState:
@@ -133,6 +135,12 @@ def __init__(self):
FRAME: "frm_levelsview",
VISIBLE: True,
},
+ WDGSIGNALS: {
+ CLASS: SignalsviewFrame,
+ FRAME: "frm_signalsview",
+ VISIBLE: False,
+ COLSPAN: 2,
+ },
WDGMAP: {
DEFAULT: True,
CLASS: MapviewFrame,
diff --git a/tests/test_static.py b/tests/test_static.py
index d1fc7f34..f0598250 100644
--- a/tests/test_static.py
+++ b/tests/test_static.py
@@ -132,7 +132,7 @@ def testconfiguration(self):
self.assertEqual(cfg.get("lbandclientdrat_n"), 2400)
self.assertEqual(cfg.get("userport_s"), "")
self.assertEqual(cfg.get("spartnport_s"), "")
- self.assertEqual(len(cfg.settings), 152)
+ self.assertEqual(len(cfg.settings), 155)
kwargs = {"userport": "/dev/ttyACM0", "spartnport": "/dev/ttyACM1"}
cfg.loadcli(**kwargs)
self.assertEqual(cfg.get("userport_s"), "/dev/ttyACM0")