Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,12 +175,13 @@ For more comprehensive installation instructions, please refer to [INSTALLATION.
31. The behaviour of Toplevel ('pop-up') dialogs will depend on the screen resolution and 'transient' setting. If the width or height of a Toplevel dialog exceeds the screen resolution, the dialog will be displayed in a scrollable, resizeable window. Otherwise, the dialog is displayed as a fixed, non-resizeable panel.
- A boolean configuration setting `transient_dialog_b` governs whether Toplevel dialogs are 'transient' (i.e. always on top of main application dialog) or not. Changing this setting to `0` allows Toplevel dialogs to be minimised independently of the main application window, but be mindful that some dialogs may end up hidden behind others e.g. "Open file/folder" dialogs. **If a file open button appears unresponsive, check that the "Open file/folder" panel isn't already open but obscured**.
- If you're accessing the desktop via a VNC session (e.g. to a headless Raspberry Pi) it is recommended to keep the setting at the default `1`, as VNC may not recognise keystrokes on overlaid non-transient windows.
- A boolean configuration setting `resizeable_dialog_b` governs whether *all* Toplevel dialogs are resizeable, irrespective of the default setting in `DialogState`.

#### <a name="widgets">User-selectable widgets</a>
---
| Widget | To show or hide the various widgets, go to Menu..View and click on the relevant hide/show option. |
|---------------------------|---------------------------------------------------------------------------------------------------|
|![banner widget](https://github.com/semuconsulting/PyGPSClient/blob/master/images/banner_widget.png?raw=true)| 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 ![expand icon](https://github.com/semuconsulting/PyGPSClient/blob/master/src/pygpsclient/resources/iconmonstr-arrow-80-16.png?raw=true)/![expand icon](https://github.com/semuconsulting/PyGPSClient/blob/master/src/pygpsclient/resources/iconmonstr-triangle-1-16.png?raw=true) 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; UNI: BESTNAV, SATSINFO, STADOP |
|![banner widget](https://github.com/semuconsulting/PyGPSClient/blob/master/images/banner_widget.png?raw=true)| 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 ![expand icon](https://github.com/semuconsulting/PyGPSClient/blob/master/src/pygpsclient/resources/iconmonstr-arrow-80-16.png?raw=true)/![expand icon](https://github.com/semuconsulting/PyGPSClient/blob/master/src/pygpsclient/resources/iconmonstr-triangle-1-16.png?raw=true) buttons. Ordinarily 'track:' represents heading of motion (aka course over ground), but the field will display 'hdg:' where static heading (yaw) is available. **NB**: some fields (e.g. hdop/vdop, hacc/vacc) are only available from proprietary NMEA or UBX messages and may not be output by default. The minimum messages required to populate all available fields are: NMEA: GGA, GSA, GSV, RMC, UBX00 (proprietary); UBX: NAV-DOP, NAV-PVT, NAV-SAT; UNI: BESTNAV, SATSINFO, STADOP. |
|![console widget](https://github.com/semuconsulting/PyGPSClient/blob/master/images/console_widget.png?raw=true)| 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](https://github.com/semuconsulting/PyGPSClient/blob/master/images/skyview_widget.png?raw=true)| Skyview widget showing current satellite visibility and position (elevation / azimuth). Satellite icon borders are colour-coded to distinguish between different GNSS constellations. For consistency between NMEA and UBX data sources, will display GLONASS NMEA SVID (65-96) rather than slot (1-24). |
|![levelsview widget](https://github.com/semuconsulting/PyGPSClient/blob/master/images/graphview_widget.png?raw=true)| 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. |
Expand All @@ -194,7 +195,7 @@ For more comprehensive installation instructions, please refer to [INSTALLATION.
|![scatterplot widget](https://github.com/semuconsulting/PyGPSClient/blob/master/images/scatterplot_widget.png?raw=true)| Scatterplot widget showing variability in position reporting over time. (Optional) Enter fixed reference position. Select Average to center plot on dynamic average position (*displayed at top left*), or Fixed to center on fixed reference position (*if entered*). Check Autorange to set plot range automatically. Set the update interval (e.g. 4 = every 4th navigation solution). Use the range slider or mouse wheel to adjust plot range. Right-click to set fixed reference point to the current mouse cursor position. Double-click to clear the existing data. |
|![rover widget](https://github.com/semuconsulting/PyGPSClient/blob/master/images/rover_widget.png?raw=true) | Rover widget plots the relative 2D position, track and status information for the roving receiver in a fixed or moving base / rover RTK configuration. Can also display relative position of NTRIP mountpoint and receiver in a static RTK configuration. Double-click to clear existing plot. |
|![chart view](https://github.com/semuconsulting/PyGPSClient/blob/master/images/chart_widget.png?raw=true) | Chart widget acts as a multi-channel "plotter", allowing the user to plot a series of named numeric data attributes from any parsed GNSS data source, with configurable y (value) and x (time) axes. By default, the number of channels is set to 4, but this can be manually edited by the user via the json configuration file setting `chartsettings_d["numchn_n"]`. For each channel, user can select: (*optional*) identity of message source e.g. `NAV-PVT`; attribute name e.g. `hAcc`; scaling factor (divisor) e.g. 1000; y axis range e.g. 0 - 5. Wildcards are available for attribute groups - "\*" (average of group values), "+" (maximum of group values), "-" (minimum of group values) e.g. `cno*` will plot the average `cno` value for a group of satellites. Double-click to clear the existing data. Double-right-click to save the current chart data to the clipboard in CSV format, which can be directly pasted into a spreadsheet application. |
|![IMU widget](https://github.com/semuconsulting/PyGPSClient/blob/master/images/imu_widget.png?raw=true) | IMU (Inertial Management Unit) Monitor widget showing current orientation/attitude (roll, pitch, yaw) and status of IMU from any IMU/Dead Reckoning message source. Select range in degrees (from ±1 to ±180 degrees). |
|![attitude widget](https://github.com/semuconsulting/PyGPSClient/blob/master/images/attitude_widget.png?raw=true) | Attitude Monitor widget (*formerly "IMU Monitor"*) showing current orientation/attitude (roll, pitch, yaw *aka 'static heading'*) and status from a variety of IMU, Dead Reckoning, Dual Antenna or other 2D/3D attitude message sources. Select range in degrees (from ±1 to ±180 degrees). |

---
## <a name="ubxconfig">UBX Configuration Facilities</a>
Expand Down
5 changes: 5 additions & 0 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# PyGPSClient Release Notes

### RELEASE 1.6.9

1. Add manually-editable `resizeable_dialog_b` configuration setting as a workaround for screen scaling issues on some platforms (e.g. Ubuntu Wayland). Fixes #250.
1. IMU Monitor renamed to Attitude Monitor, and various internal enhancements including explicit display of NAV-PVT `headVehValid` status.

### RELEASE 1.6.8

1. Updates to experimental RINEX conversion dialog for pygnssutils >=1.2.1. Provisional support for RINEX 4.02 and GPS CNAV added.
Expand Down
16 changes: 8 additions & 8 deletions docs/pygpsclient.rst
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,14 @@ pygpsclient.app module
:undoc-members:
:show-inheritance:

pygpsclient.attitude\_frame module
----------------------------------

.. automodule:: pygpsclient.attitude_frame
:members:
:undoc-members:
:show-inheritance:

pygpsclient.banner\_frame module
--------------------------------

Expand Down Expand Up @@ -148,14 +156,6 @@ pygpsclient.importmap\_dialog module
:undoc-members:
:show-inheritance:

pygpsclient.imu\_frame module
-----------------------------

.. automodule:: pygpsclient.imu_frame
:members:
:undoc-members:
:show-inheritance:

pygpsclient.init\_presets module
--------------------------------

Expand Down
File renamed without changes
1 change: 1 addition & 0 deletions pygpsclient.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"IMU Monitor": false,
"checkforupdate_b": 0,
"transient_dialog_b": 1,
"resize_dialog_b": 0,
"guiupdateinterval_f": 0.2,
"mapupdateinterval_n": 60,
"defaultport_s": "USB",
Expand Down
2 changes: 1 addition & 1 deletion src/pygpsclient/_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@
:license: BSD 3-Clause
"""

__version__ = "1.6.8"
__version__ = "1.6.9"
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
"""
imu_frame.py
attitude_frame.py

IMU (Inertial Management Unit) frame class for PyGPSClient Application.
Attitude (3D orientation) frame class for PyGPSClient Application.

This show orientiation (Pitch, Roll, Yaw) and calibration status of
any UBX or NMEA IMU source.
a variety of sources.

Created 23 March 2023

Expand Down Expand Up @@ -43,7 +43,7 @@
WIDGETU2,
)
from pygpsclient.helpers import rgb2str, scale_font
from pygpsclient.strings import DLGWAITIMU
from pygpsclient.strings import DLGWAITATTITUDE

OFFSETX = 5
OFFSETY = 10
Expand All @@ -68,9 +68,9 @@
OPTIONS = ("N/A",)


class IMUFrame(Frame):
class AttitudeFrame(Frame):
"""
IMU (Inertial Management Unit) frame class.
Attitude (3D orientation) frame class.
"""

def __init__(self, app: Frame, parent: Frame, *args, **kwargs):
Expand Down Expand Up @@ -403,7 +403,7 @@ def _on_waiting(self):
"""

if self._waiting:
self._canvas.create_alert(DLGWAITIMU, tags=TAG_WAIT)
self._canvas.create_alert(DLGWAITATTITUDE, tags=TAG_WAIT)

def get_size(self) -> tuple:
"""
Expand Down
2 changes: 2 additions & 0 deletions src/pygpsclient/banner_frame.py
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,8 @@ def _update_track(self, units):
else:
self._lbl_spd.config(text=NA)
track = self.__app.gnss_status.track
# ltrk = " hdg:" if self.__app.gnss_status.headvehvalid else " track:"
# self._lbl_ltrk.config(text=ltrk)
if isinstance(track, (int, float)):
self._lbl_trk.config(text=f"{track:05.1f} °")
else:
Expand Down
1 change: 1 addition & 0 deletions src/pygpsclient/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ def __init__(self, app):
**self.widget_config,
"checkforupdate_b": 1,
"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,
"defaultport_s": RCVR_CONNECTION,
Expand Down
3 changes: 2 additions & 1 deletion src/pygpsclient/gnss_status.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ def reset(self):
self.alt = 0.0 # height above sea level m
self.hae = 0.0 # height above ellipsoid m
self.speed = 0.0 # speed m/s
self.track = 0.0 # track degrees
self.track = 0.0 # track (heading of motion or course over ground) degrees
self.headvehvalid = 0 # 0 = heading of motion, 1 = static heading (yaw)
self.fix = "NO FIX" # fix type e.g. "3D"
self.siv = 0 # satellites in view
self.sip = 0 # satellites in position solution
Expand Down
15 changes: 15 additions & 0 deletions src/pygpsclient/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -684,6 +684,21 @@ def get_units(units: str) -> tuple:
return dst_u, dst_c, ele_u, ele_c, spd_u, spd_c


def hdg2yaw(heading: float) -> float:
"""
Convert heading (0 - 360) to yaw (-180 - 180)

:param float heading: heading in range 0 - 360
:return: yaw in range -180 to 180
:rtype: float
"""

heading %= 360
if heading > 180:
heading -= 360
return heading


def hsv2rgb(h: float, s: float, v: float) -> str:
"""
Convert HSV values (in range 0-1) to RGB color string.
Expand Down
6 changes: 4 additions & 2 deletions src/pygpsclient/nmea_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@

from pygpsclient.dialog_state import DLGTSERVER
from pygpsclient.globals import SAT_EXPIRY, TKGN
from pygpsclient.helpers import fix2desc, kmph2ms, knots2ms, svid2gnssid
from pygpsclient.helpers import fix2desc, hdg2yaw, kmph2ms, knots2ms, svid2gnssid
from pygpsclient.strings import DLGTNMEA, NA


Expand Down Expand Up @@ -152,6 +152,7 @@ def _process_RMC(self, data: NMEAMessage):
self.__app.gnss_status.speed = knots2ms(data.spd) # convert to m/s
if data.cog != "":
self.__app.gnss_status.track = data.cog
self.__app.gnss_status.headvehvalid = 0

def _process_GGA(self, data: NMEAMessage):
"""
Expand Down Expand Up @@ -289,6 +290,7 @@ def _process_VTG(self, data: NMEAMessage):
if data.sogk is not None:
self.__app.gnss_status.speed = kmph2ms(data.sogk) # convert to m/s
self.__app.gnss_status.fix = fix2desc("VTG", data.posMode)
self.__app.gnss_status.headvehvalid = 0
# only works for NMEA 4.10 and later...
# self.__app.gnss_status.diff_corr = 1 if data.posMode == "D" else 0

Expand Down Expand Up @@ -518,7 +520,7 @@ def _process_IMU(self, data: NMEAMessage):
ims["pitch"] = round(getattr(data, "pitch", 0), 4)
ims["yaw"] = round(getattr(data, "yaw", 0), 4)
if hasattr(data, "heading"): # range 0 - 360
ims["yaw"] = round(data.heading - 180, 4)
ims["yaw"] = round(hdg2yaw(data.heading), 4)
ims["status"] = str(getattr(data, "quality", ""))
except (TypeError, KeyError, AttributeError):
pass
Expand Down
6 changes: 4 additions & 2 deletions src/pygpsclient/rinex_dialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
:license: BSD 3-Clause
"""

# pylint: disable=use-implicit-booleaness-not-comparison

from datetime import datetime, timezone
from logging import getLogger
from pathlib import Path
Expand Down Expand Up @@ -829,7 +831,7 @@ def _do_conversion(self, **kwargs):
]
observer = self._observer.get()
doi = self._doi.get()
license = self._license.get()
licen = self._license.get()
station = self._station.get()
comments = ["PyGPSClient RINEX Converter Dialog"]
country = self._countrycode.get()
Expand Down Expand Up @@ -859,7 +861,7 @@ def _do_conversion(self, **kwargs):
comments=comments,
protfilter=protfilter,
doi=doi,
license=license,
license=licen,
station=station,
country=country,
**kwargs,
Expand Down
2 changes: 1 addition & 1 deletion src/pygpsclient/sbf_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
from pygpsclient.helpers import fix2desc
from pygpsclient.strings import NA

DNUL = -2 * (10**10)
DNUL = -20000000000 # -2 * (10**10)
DNUS = 65535


Expand Down
2 changes: 1 addition & 1 deletion src/pygpsclient/strings.py
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@
DLGTUBX = "UBX Configuration"
DLGWAITAZI = "Waiting for ELV/AZI data"
DLGWAITCNO = "Waiting for CNO data"
DLGWAITIMU = "Waiting for IMU data"
DLGWAITATTITUDE = "Waiting for Attitude data"
DLGWAITMONSPAN = "Waiting for UBX MON-SPAN data"
DLGWAITMONSYS = "Waiting for UBX MON-SYS data"
DLGWAITNAVSIG = "Waiting for UBX NAV-SIG data"
Expand Down
10 changes: 7 additions & 3 deletions src/pygpsclient/toplevel_dialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,13 @@ def __init__(self, app, dlgname: str, *args, **kwargs):
self._dlgname = dlgname
self.logger = logging.getLogger(f"{APPNAME}.{dlgname}")
self.width, self.height = 300, 300 # initial, updated in finalise()
self._resizable = kwargs.pop(
"resizable", self.__app.dialog_state.state[self._dlgname].get(RESIZE, False)
)
if self.__app.configuration.get("resizeable_dialog_b") == 1:
self._resizable = True
else:
self._resizable = kwargs.pop(
"resizable",
self.__app.dialog_state.state[self._dlgname].get(RESIZE, False),
)
transient = kwargs.pop(
"transient", self.__app.configuration.get("transient_dialog_b")
)
Expand Down
42 changes: 32 additions & 10 deletions src/pygpsclient/ubx_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
from pyubx2 import UBXMessage, itow2utc

from pygpsclient.globals import BSR, GLONASS_NMEA, UTF8
from pygpsclient.helpers import corrage2int, fix2desc, ned2vector, svid2gnssid
from pygpsclient.helpers import corrage2int, fix2desc, hdg2yaw, ned2vector, svid2gnssid
from pygpsclient.strings import DLGTSERVER, DLGTSPARTN, DLGTUBX, NA
from pygpsclient.widget_state import VISIBLE, WDGSIGNALS, WDGSPECTRUM, WDGSYSMON

Expand Down Expand Up @@ -288,6 +288,7 @@ def _process_NAV_PVT(self, data: UBXMessage):
self.__app.gnss_status.sip = data.numSV
self.__app.gnss_status.speed = data.gSpeed / 1000 # m/s
self.__app.gnss_status.track = data.headMot
self.__app.gnss_status.headvehvalid = data.headVehValid
self.__app.gnss_status.hae = data.height / 1000 # meters
self.__app.gnss_status.fix = fix2desc("NAV-PVT", data.fixType)
if data.carrSoln > 0:
Expand All @@ -300,6 +301,27 @@ def _process_NAV_PVT(self, data: UBXMessage):
if data.lastCorrectionAge != 0:
self.__app.gnss_status.diff_age = corrage2int(data.lastCorrectionAge)

ims = self.__app.gnss_status.imu_data
if data.headVehValid == 1:
# self.__app.gnss_status.track = data.headVeh
ims["source"] = data.identity
ims["roll"] = 0
ims["pitch"] = 0
ims["yaw"] = round(hdg2yaw(data.headVeh), 4)
ims["status"] = "HEADVEHVALID"
self.__app.gnss_status.imu_data = ims
else:
if (
ims.get("source", "") == "NAV-PVT"
and ims.get("status", "") == "HEADVEHVALID"
):
ims["source"] = NA
ims["roll"] = 0
ims["pitch"] = 0
ims["yaw"] = 0
ims["status"] = ""
self.__app.gnss_status.imu_data = ims

def _process_NAV_PVAT(self, data: UBXMessage):
"""
Process NAV-PVAT sentence - Navigation position velocity attitude time solution.
Expand All @@ -316,9 +338,9 @@ def _process_NAV_PVAT(self, data: UBXMessage):
self.__app.gnss_status.hae = data.height / 1000 # meters
ims = self.__app.gnss_status.imu_data
ims["source"] = data.identity
ims["roll"] = data.vehRoll
ims["pitch"] = data.vehPitch
ims["yaw"] = data.vehHeading
ims["roll"] = round(data.vehRoll, 4)
ims["pitch"] = round(data.vehPitch, 4)
ims["yaw"] = round(hdg2yaw(data.vehHeading), 4)
ims["status"] = (
(data.vehRollValid << 3) + (data.vehPitchValid << 2) + data.vehHeadingValid
)
Expand Down Expand Up @@ -632,9 +654,9 @@ def _process_NAV_ATT(self, data: UBXMessage):

ims = self.__app.gnss_status.imu_data
ims["source"] = data.identity
ims["roll"] = data.roll
ims["pitch"] = data.pitch
ims["yaw"] = data.heading
ims["roll"] = round(data.roll, 4)
ims["pitch"] = round(data.pitch, 4)
ims["yaw"] = round(hdg2yaw(data.heading), 4)
ims["status"] = ""

def _process_HNR_ATT(self, data: UBXMessage):
Expand All @@ -646,7 +668,7 @@ def _process_HNR_ATT(self, data: UBXMessage):

ims = self.__app.gnss_status.imu_data
ims["source"] = data.identity
ims["roll"] = data.roll
ims["pitch"] = data.pitch
ims["yaw"] = data.heading
ims["roll"] = round(data.roll, 4)
ims["pitch"] = round(data.pitch, 4)
ims["yaw"] = round(hdg2yaw(data.heading), 4)
ims["status"] = ""
Loading