From 8f9771faecc65366cdb45bb96142410884c49d81 Mon Sep 17 00:00:00 2001
From: SEMU Admin <28569967+semuadmin@users.noreply.github.com>
Date: Thu, 5 Feb 2026 18:06:03 +0000
Subject: [PATCH 1/4] add unicore support
---
RELEASE_NOTES.md | 4 ++++
pyproject.toml | 2 +-
src/pygpsclient/_version.py | 2 +-
src/pygpsclient/app.py | 9 +++++----
src/pygpsclient/globals.py | 5 ++---
src/pygpsclient/helpers.py | 2 ++
src/pygpsclient/settings_child_frame.py | 13 +++++++------
src/pygpsclient/stream_handler.py | 7 ++++---
8 files changed, 26 insertions(+), 18 deletions(-)
diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md
index c5718b1c..e386809b 100644
--- a/RELEASE_NOTES.md
+++ b/RELEASE_NOTES.md
@@ -1,5 +1,9 @@
# PyGPSClient Release Notes
+### RELEASE 1.6.3
+
+1. Add support for Unicore UNI binary data output messages via `pyunigps>=0.1.2` and `pygnssutils>=1.1.22`.
+
### RELEASE 1.6.2
1. Add support for Unicore Secondary Antenna and Attitude (IMU) NMEA sentences e.g. UM98n "GGAH", "HPD" (requires pynmeagps>=1.1.0).
diff --git a/pyproject.toml b/pyproject.toml
index 297f985b..7d33d216 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -49,7 +49,7 @@ classifiers = [
"Topic :: Scientific/Engineering :: GIS",
]
-dependencies = ["requests>=2.28.0", "Pillow>=9.0.0", "pygnssutils>=1.1.21"]
+dependencies = ["requests>=2.28.0", "Pillow>=9.0.0", "pygnssutils>=1.1.22"]
[project.scripts]
pygpsclient = "pygpsclient.__main__:main"
diff --git a/src/pygpsclient/_version.py b/src/pygpsclient/_version.py
index 99d1f787..4a3417a2 100644
--- a/src/pygpsclient/_version.py
+++ b/src/pygpsclient/_version.py
@@ -8,4 +8,4 @@
:license: BSD 3-Clause
"""
-__version__ = "1.6.2"
+__version__ = "1.6.3"
diff --git a/src/pygpsclient/app.py b/src/pygpsclient/app.py
index 25fa8e3c..5fc4c660 100644
--- a/src/pygpsclient/app.py
+++ b/src/pygpsclient/app.py
@@ -48,13 +48,14 @@
from types import NoneType
from pygnssutils import GNSSMQTTClient, GNSSNTRIPClient, MQTTMessage
-from pygnssutils.gnssreader import ( # UNI_PROTOCOL, # TODO
+from pygnssutils.gnssreader import (
NMEA_PROTOCOL,
POLL,
QGC_PROTOCOL,
RTCM3_PROTOCOL,
SBF_PROTOCOL,
UBX_PROTOCOL,
+ UNI_PROTOCOL,
)
from pygnssutils.socket_server import ClientHandler, ClientHandlerTLS, SocketServer
from pynmeagps import NMEAMessage
@@ -63,6 +64,7 @@
from pysbf2 import SBFMessage
from pyspartn import SPARTNMessage
from pyubx2 import UBXMessage
+from pyunigps import UNIMessage
from serial import SerialException, SerialTimeoutException
from pygpsclient._version import __version__ as VERSION
@@ -97,7 +99,6 @@
STATUSPRIORITY,
TTY_PROTOCOL,
UNDO,
- UNI_PROTOCOL,
)
from pygpsclient.gnss_status import GNSSStatus
from pygpsclient.helpers import (
@@ -913,8 +914,8 @@ def process_data(self, raw_data: bytes, parsed_data: object, marker: str = ""):
msgprot = SBF_PROTOCOL
elif isinstance(parsed_data, QGCMessage):
msgprot = QGC_PROTOCOL
- # elif isinstance(parsed_data, UNIMessage):
- # msgprot = UNI_PROTOCOL
+ elif isinstance(parsed_data, UNIMessage):
+ msgprot = UNI_PROTOCOL
elif isinstance(parsed_data, UBXMessage):
msgprot = UBX_PROTOCOL
elif isinstance(parsed_data, RTCMMessage):
diff --git a/src/pygpsclient/globals.py b/src/pygpsclient/globals.py
index f6c96df1..8c08f9df 100644
--- a/src/pygpsclient/globals.py
+++ b/src/pygpsclient/globals.py
@@ -53,7 +53,6 @@
PNTCOL = "#FF8000" # default plot point color
# Protocols to be used in protocol mask (others defined in pygnssutils.gnss_reader.py)
-UNI_PROTOCOL = 32 # provisional - awaiting pygnssutils.gnssreader updates for Unicore
SPARTN_PROTOCOL = 256
MQTT_PROTOCOL = 512
TTY_PROTOCOL = 1024
@@ -251,8 +250,8 @@
TOPIC_RXM = "/pp/ubx/0236/ip"
TRACK = "track"
TRACEMODE_WRITE = "write"
-TTYOK = ("OK", "$R:")
-TTYERR = ("ERROR", "$R?")
+TTYOK = ("OK", "$R:", "RESPONSE: OK")
+TTYERR = ("ERROR", "$R?", "RESPONSE: PARSING FAILD")
TTYMARKER = "TTY<<"
UBXPRESETS = "ubxpresets"
UBXSIMULATOR = "ubxsimulator"
diff --git a/src/pygpsclient/helpers.py b/src/pygpsclient/helpers.py
index 2ce8baff..ddd17930 100644
--- a/src/pygpsclient/helpers.py
+++ b/src/pygpsclient/helpers.py
@@ -59,6 +59,7 @@
atttyp,
)
from pyubx2 import version as UBXVERSION
+from pyunigps import version as UNIVERSION
from requests import get
from pygpsclient._version import __version__ as VERSION
@@ -107,6 +108,7 @@
"pygnssutils": PGVERSION,
"pyubx2": UBXVERSION,
"pysbf2": SBFVERSION,
+ "pyunigps": UNIVERSION,
"pyqgc": QGCVERSION,
"pynmeagps": NMEAVERSION,
"pyrtcm": RTCMVERSION,
diff --git a/src/pygpsclient/settings_child_frame.py b/src/pygpsclient/settings_child_frame.py
index e6e928b8..d0e0c2eb 100644
--- a/src/pygpsclient/settings_child_frame.py
+++ b/src/pygpsclient/settings_child_frame.py
@@ -456,11 +456,11 @@ def _do_layout(self):
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_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._chk_unicore.grid(column=2, row=2, padx=0, pady=0, sticky=W) # TODO
+ self._chk_unicore.grid(column=1, row=1, padx=0, pady=0, sticky=W)
+ self._chk_sbf.grid(column=2, row=1, padx=0, pady=0, sticky=W)
+ self._chk_qgc.grid(column=3, row=1, padx=0, pady=0, sticky=W)
+ self._chk_spartn.grid(column=1, row=2, padx=0, pady=0, sticky=W)
+ self._chk_tty.grid(column=2, row=2, padx=0, pady=0, sticky=W)
self._lbl_consoledisplay.grid(column=0, row=3, padx=2, pady=2, sticky=W)
self._spn_conformat.grid(
column=1, row=3, columnspan=2, padx=1, pady=2, sticky=W
@@ -536,6 +536,7 @@ def reset(self):
self._prot_ubx.set(cfg.get("ubxprot_b"))
self._prot_sbf.set(cfg.get("sbfprot_b"))
self._prot_qgc.set(cfg.get("qgcprot_b"))
+ self._prot_unicore.set(cfg.get("uniprot_b"))
self._prot_rtcm.set(cfg.get("rtcmprot_b"))
self._prot_spartn.set(cfg.get("spartnprot_b"))
self._prot_tty.set(cfg.get("ttyprot_b"))
@@ -588,7 +589,7 @@ def _on_update_qgcprot(self, var, index, mode):
def _on_update_uniprot(self, var, index, mode):
"""
- Action on updating unicoreprot.
+ Action on updating uniprot.
"""
if not self._prot_tty.get():
diff --git a/src/pygpsclient/stream_handler.py b/src/pygpsclient/stream_handler.py
index e26e3f65..0e245272 100644
--- a/src/pygpsclient/stream_handler.py
+++ b/src/pygpsclient/stream_handler.py
@@ -47,12 +47,13 @@ class to read and parse incoming data from the receiver. It places
from tkinter import Frame, Label, Tk
from certifi import where as findcacerts
-from pygnssutils import ( # UNI_PROTOCOL # TODO
+from pygnssutils import (
NMEA_PROTOCOL,
QGC_PROTOCOL,
RTCM3_PROTOCOL,
SBF_PROTOCOL,
UBX_PROTOCOL,
+ UNI_PROTOCOL,
GNSSError,
GNSSReader,
)
@@ -322,8 +323,8 @@ def _errorhandler(err: Exception):
| UBX_PROTOCOL
| SBF_PROTOCOL
| QGC_PROTOCOL
- | RTCM3_PROTOCOL,
- # | UNI_PROTOCOL, # TODO
+ | RTCM3_PROTOCOL
+ | UNI_PROTOCOL,
quitonerror=ERR_LOG,
bufsize=DEFAULT_BUFSIZE,
msgmode=settings["msgmode"],
From 2a282f21ed72a530b4a4d25a3341af243ead736b Mon Sep 17 00:00:00 2001
From: SEMU Admin <28569967+semuadmin@users.noreply.github.com>
Date: Mon, 9 Feb 2026 12:42:46 +0000
Subject: [PATCH 2/4] update docs
---
README.md | 4 ++--
RELEASE_NOTES.md | 2 +-
src/pygpsclient/init_presets.py | 10 ++++++++++
src/pygpsclient/strings.py | 4 ++--
4 files changed, 15 insertions(+), 5 deletions(-)
diff --git a/README.md b/README.md
index ac7442a9..7f8334d6 100644
--- a/README.md
+++ b/README.md
@@ -20,7 +20,7 @@
PyGPSClient is a free, open-source, multi-platform graphical GNSS/GPS testing, diagnostic and configuration application written entirely by volunteers in Python and tkinter.
* Runs on any platform which supports a Python 3 interpreter (>=3.10) and tkinter (>=8.6) GUI framework, including Windows, MacOS, Linux and Raspberry Pi OS. Accommodates low resolution screens (>= 640x480) via resizable and/or scrollable panels.
-* Supports NMEA, UBX, SBF, QGC, RTCM3, NTRIP, SPARTN, MQTT and TTY (ASCII) protocols¹.
+* Supports NMEA, UBX (u-blox binary), SBF (Septentrio binary), UNI (Unicore binary), QGC (Quectel binary), RTCM3, NTRIP, SPARTN, MQTT and TTY (ASCII) protocols¹.
* Capable of reading from a variety of GNSS data streams: Serial (USB / UART), Socket (TCP / UDP), binary data stream (terminal or file capture) and binary recording (e.g. u-center \*.ubx).
* Provides [NTRIP](#ntripconfig) client facilities.
* Can serve as an [NTRIP base station](#basestation) with an RTK-compatible receiver (e.g. u-blox ZED-F9P/ZED-X20P, Quectel LG290P/LG580P/LC29H, Septentrio Mosaic G5/X5 or Unicore UM98n).
@@ -116,7 +116,7 @@ For more comprehensive installation instructions, please refer to [INSTALLATION.
1. To immediately disconnect and terminate all running threads, click Ctrl-K ("Kill Switch").
1. To exit the application, click
, or press Ctrl-Q, or click the application window's close window icon.
-1. Protocols Shown - Select which protocols to display; NMEA, UBX, SBF, QGC, RTCM3, SPARTN or TTY (NB: this only changes the displayed protocols - to change the actual protocols output by the receiver, use the [UBX Configuration Dialog](#ubxconfig)).
+1. Protocols Shown - Select which protocols to display; NMEA, UBX (*u-blox binary*), SBF (*Septentrio binary*), UNI (*Unicore binary*), QGC (*Quectel binary*), RTCM3, SPARTN or TTY (*ASCII text*) (NB: this only changes the displayed protocols - to change the actual protocols output by the receiver, use the relevant configuration command(s)).
- **NB:** Serial connection must be stopped before changing to or from TTY (terminal) protocol.
- **NB:** Enabling TTY (terminal) mode will disable all other protocols.
1. Console Display - Select console display format (Parsed, Binary, Hex Tabular, Hex String, Parsed+Hex Tabular - see Console Widget below).
diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md
index e386809b..6f88ce0b 100644
--- a/RELEASE_NOTES.md
+++ b/RELEASE_NOTES.md
@@ -2,7 +2,7 @@
### RELEASE 1.6.3
-1. Add support for Unicore UNI binary data output messages via `pyunigps>=0.1.2` and `pygnssutils>=1.1.22`.
+1. Add support for Unicore UNI binary data output messages via `pyunigps>=0.1.3` and `pygnssutils>=1.1.22`.
### RELEASE 1.6.2
diff --git a/src/pygpsclient/init_presets.py b/src/pygpsclient/init_presets.py
index 9b6d0624..558e8838 100644
--- a/src/pygpsclient/init_presets.py
+++ b/src/pygpsclient/init_presets.py
@@ -99,8 +99,18 @@
"Septentrio X5 Stop RTCM output;setDataInOut,COM1, ,none",
"Unicore UM98n Restore Factory Defaults CONFIRM; freset",
"Unicore UM98n Save Current Configuration to NVM CONFIRM; saveconfig",
+ "Unicore UM98n Force COLD reset CONFIRM; reset",
+ "Unicore UM98n Enable NMEA on current port; gpgsa 1; gpgga 1; gpgll 1; gpgsv 4; gpvtg 1; gprmc 1",
+ "Unicore UM98n Enable UNI on current port; PVTSLNB 1; SATELLITEB 4; BESTNAV 1; STADOP 1",
+ "Unicore UM98n Query Version; version",
+ "Unicore UM98n Query Config; config",
"Unicore UM98n Stop All Output on COM1; unlog COM1",
"Unicore UM98n Stop All Output on COM2; unlog COM2",
+ "Unicore UM98n Set COM1/2 Baud Rate to 115200; config com1 115200; config com2 115200",
+ "Unicore UM98n Set COM1/2 Baud Rate to 460800; config com1 460800; config com2 460800",
+ "Unicore UM98n Set Rover Mode CONFIRM; mode rover; saveconfig",
+ "Unicore UM98n Set Base Mode Survey-In CONFIRM; mode base time 60; rtcm1006 com1 10; rtcm1033 com1 10; rtcm1074 com1 1; rtcm1124 com1 1; rtcm1084 com1 1; rtcm1094 com1 1; saveconfig",
+ "Unicore UM98n Enable B2p PPP; CONFIG PPP ENABLE B2b-PPP; CONFIG PPP CONVERGE 10 20"
"Feyman IM19 Tilt Survey Setup; AT+LOAD_DEFAULT; AT+GNSS_PORT=PHYSICAL_UART2; AT+NASC_OUTPUT=UART1,ON; AT+LEVER_ARM2=0.0057,-0.0732,-0.0645; AT+CLUB_VECTOR=0,0,1.865; AT+INSTALL_ANGLE=0,180,0; AT+GNSS_CARD=OEM; AT+WORK_MODE=408; AT+CORRECT_HOLDER=ENABLE; AT+SET_PPS_EDGE=RISING; AT+AHRS=ENABLE; AT+MAG_AUTO_SAVE=ENABLE; AT+SAVE_ALL",
"Feyman IM19 System reset CONFIRM; AT+SYSTEM_RESET",
"Feyman IM19 Save the parameters CONFIRM; AT+SAVE_ALL",
diff --git a/src/pygpsclient/strings.py b/src/pygpsclient/strings.py
index f08ef8b1..e6aef7f0 100644
--- a/src/pygpsclient/strings.py
+++ b/src/pygpsclient/strings.py
@@ -25,8 +25,8 @@
ABOUTTXT = [
f"{TITLE} is a free, open-source GNSS diagnostic and configuration",
- "application written entirely by volunteers in Python and tkinter.",
- "It supports NMEA, UBX, SBF, QGC, RTCM3, NTRIP & SPARTN protocols.",
+ "application written entirely by volunteers in Python and tkinter. It",
+ "supports NMEA, UBX, SBF, UNI, QGC, RTCM3, NTRIP & SPARTN protocols.",
"Instructions and source code are available on GitHub at the link below.",
]
NA = "N/A"
From 22132136802be14c044971826c6e7f0a3ac2be21 Mon Sep 17 00:00:00 2001
From: SEMU Admin <28569967+semuadmin@users.noreply.github.com>
Date: Tue, 10 Feb 2026 09:58:10 +0000
Subject: [PATCH 3/4] update uni_handler
---
src/pygpsclient/globals.py | 24 +++-
src/pygpsclient/helpers.py | 12 +-
src/pygpsclient/init_presets.py | 2 +-
src/pygpsclient/skyview_frame.py | 11 +-
src/pygpsclient/uni_handler.py | 195 ++++++++++++++++++++++++++++++-
tests/test_static.py | 24 ++--
6 files changed, 235 insertions(+), 33 deletions(-)
diff --git a/src/pygpsclient/globals.py b/src/pygpsclient/globals.py
index 8c08f9df..7042d4cf 100644
--- a/src/pygpsclient/globals.py
+++ b/src/pygpsclient/globals.py
@@ -423,11 +423,31 @@
"PVTGeodetic7": "RTK FIXED",
"PVTGeodetic8": "RTK FLOAT",
"PVTGeodetic10": "PPP",
+ "BESTNAV0": "NO FIX",
+ "BESTNAV1": "FIXEDPOS", # FIXEDPOS Position fixed by the FIX POSITION command
+ "BESTNAV2": "FIXEDHMSL", # FIXEDHEIGHT Not supported currently
+ "BESTNAV8": "DOPP", # DOPPLER_VELOCITY Velocity computed using instantaneous Doppler
+ "BESTNAV16": "3D", # SINGLE Single point positioning
+ "BESTNAV17": "RTK FLOAT", # SRDIFF Pseudorange differential solution
+ "BESTNAV18": "SBAS", # SBAS SBAS positioning
+ "BESTNAV32": "RTK FLOAT", # L1_FLOAT L1 float solution
+ "BESTNAV33": "RTK FLOAT", # IONOFREE_FLOAT Ionosphere-free float solution
+ "BESTNAV34": "RTK FLOAT", # NARROW_FLOAT Narrow-lane float solution
+ "BESTNAV48": "RTK FIXED", # L1_INT L1 fixed solution
+ "BESTNAV49": "RTK FIXED", # WIDE_INT Wide-lane fixed solution
+ "BESTNAV50": "RTK FIXED", # NARROW_INT Narrow-lane fixed solution
+ "BESTNAV52": "DR", # INS Inertial navigation solution
+ "BESTNAV53": "3D+DR", # INS_PSRSP Integrated solution of INS and single point pos
+ "BESTNAV54": "RTK+DR", # INS_PSRDIFF Integrated solution of INS and pseudorange diff pos
+ "BESTNAV55": "RTK FLOAT+DR", # INS_RTKFLOAT Integrated solution of INS and RTK float
+ "BESTNAV56": "RTK FIXED+DR", # INS_RTKFIXED Integrated solution of INS and RTK fix
+ "BESTNAV68": "RTK FIXED+DR", # INS_RTKFIXED Integrated solution of INS and RTK fix
+ "BESTNAV69": "PPP", # PPP
}
"""
Map of fix values to descriptions.
-The keys in this map are a concatenation of NMEA/UBX
-message identifier and attribute value e.g.
+The keys in this map are a concatenation of
+message identity and attribute value e.g.
GGA1: GGA + quality = 1
NAV-STATUS3: NAV-STATUS + gpsFix = 3
(valid for NMEA >=4)
diff --git a/src/pygpsclient/helpers.py b/src/pygpsclient/helpers.py
index ddd17930..13fc018f 100644
--- a/src/pygpsclient/helpers.py
+++ b/src/pygpsclient/helpers.py
@@ -42,7 +42,7 @@
from typing import Any, Literal
from pygnssutils import version as PGVERSION
-from pynmeagps import WGS84_SMAJ_AXIS, NMEAMessage, haversine
+from pynmeagps import WGS84_SMAJ_AXIS, NMEAMessage, haversine, leapsecond
from pynmeagps import version as NMEAVERSION
from pyqgc import version as QGCVERSION
from pyrtcm import version as RTCMVERSION
@@ -1447,20 +1447,24 @@ def valid_geom(geom: str) -> bool:
return regexgeom.match(geom) is not None
-def wnotow2date(wno: int, tow: int) -> datetime:
+def wnotow2date(wno: int, tow: int, ls: int | NoneType = None) -> datetime:
"""
- Get datetime from GPS Week number (Wno) and Time of Week (Tow).
+ Get datetime from GPS Week number (Wno), Time of Week (Tow)
+ and leapsecond offset.
GPS Epoch 0 = 6th Jan 1980
:param int wno: week number
:param int tow: time of week
+ :param int ls: leapsecond offset
:return: datetime
:rtype: datetime
"""
+ if ls is None:
+ ls = leapsecond(datetime.now())
dat = GPSEPOCH0 + timedelta(days=wno * 7)
- dat += timedelta(seconds=tow)
+ dat += timedelta(seconds=tow - ls)
return dat
diff --git a/src/pygpsclient/init_presets.py b/src/pygpsclient/init_presets.py
index 558e8838..9b77f52b 100644
--- a/src/pygpsclient/init_presets.py
+++ b/src/pygpsclient/init_presets.py
@@ -101,7 +101,7 @@
"Unicore UM98n Save Current Configuration to NVM CONFIRM; saveconfig",
"Unicore UM98n Force COLD reset CONFIRM; reset",
"Unicore UM98n Enable NMEA on current port; gpgsa 1; gpgga 1; gpgll 1; gpgsv 4; gpvtg 1; gprmc 1",
- "Unicore UM98n Enable UNI on current port; PVTSLNB 1; SATELLITEB 4; BESTNAV 1; STADOP 1",
+ "Unicore UM98n Enable UNI on current port; PVTSLNB 1; SATSINFOB 4; BESTNAV 1; STADOP 1",
"Unicore UM98n Query Version; version",
"Unicore UM98n Query Config; config",
"Unicore UM98n Stop All Output on COM1; unlog COM1",
diff --git a/src/pygpsclient/skyview_frame.py b/src/pygpsclient/skyview_frame.py
index 7f1e40b8..e40fb7e2 100644
--- a/src/pygpsclient/skyview_frame.py
+++ b/src/pygpsclient/skyview_frame.py
@@ -110,12 +110,9 @@ def update_frame(self):
"""
data = self.__app.gnss_status.gsv_data
- show_unused = self.__app.configuration.get("unusedsat_b")
siv = len(data)
- siv = siv if show_unused else siv - unused_sats(data)
sel = sum(1 for (_, _, ele, _, _, _) in data.values() if ele not in ("", None))
- # ignore if cno are all zero and 'show_unused' is not set,
- # or if elevation values are all null
+ # ignore if elevation values are all null
if siv <= 0 or sel <= 0:
return
@@ -125,11 +122,7 @@ def update_frame(self):
for val in sorted(data.values(), key=lambda x: x[4]): # sort by ascending C/N0
try:
gnssId, prn, ele, azi, cno, _ = val
- if (
- (cno == 0 and not show_unused)
- or ele in ("", None)
- or azi in ("", None)
- ):
+ if ele in ("", None) or azi in ("", None):
continue
x, y = self._canvas.d2xy(int(azi), int(ele))
_, ol_col = GNSS_LIST[gnssId]
diff --git a/src/pygpsclient/uni_handler.py b/src/pygpsclient/uni_handler.py
index f0cddee1..8b5ce2c4 100644
--- a/src/pygpsclient/uni_handler.py
+++ b/src/pygpsclient/uni_handler.py
@@ -1,8 +1,6 @@
"""
uni_handler.py
-WORK IN PROGRESS - AWAITING PARSER DEVELOPMENT
-
Unicore GNSS Protocol handler - handles all incoming UNI messages
Parses individual UNI (Unicore UM98n GNSS) messages (using pyunignss library)
@@ -18,6 +16,31 @@
"""
import logging
+from time import time
+
+from pyunigps import UNIMessage
+
+from pygpsclient.helpers import fix2desc, wnotow2date
+
+SATSINFO_GNSSID = {
+ 0: 0, # GPS
+ 1: 6, # GLONASS
+ 2: 1, # SBAS
+ 3: 2, # GAL
+ 4: 3, # BDS
+ 5: 5, # QZSS
+ 6: 7, # IRNSS
+}
+
+SATELLITE_GNSSID = {
+ 0: 0, # GPS
+ 1: 6, # GLONASS
+ 2: 1, # SBAS
+ 5: 2, # GALILEO
+ 6: 3, # BEIDOU
+ 7: 5, # QZSS
+ 9: 7, # NAVIC
+}
class UNIHandler:
@@ -40,7 +63,7 @@ def __init__(self, app):
self._parsed_data = None
# pylint: disable=unused-argument
- def process_data(self, raw_data: bytes, parsed_data: object):
+ def process_data(self, raw_data: bytes, parsed_data: UNIMessage):
"""
Process relevant UNI message types
@@ -49,5 +72,169 @@ def process_data(self, raw_data: bytes, parsed_data: object):
"""
if raw_data is None:
- pass
+ return
# self.logger.debug(f"data received {parsed_data.identity}")
+ self.__app.gnss_status.utc = wnotow2date(
+ parsed_data.wno, int(parsed_data.tow / 1000), parsed_data.leapsecond
+ ) # datetime.time
+ if parsed_data.identity in ("BESTNAV", "BESTNAVH"):
+ self._process_BESTNAV(parsed_data)
+ elif parsed_data.identity in ("PVTSLT",):
+ self._process_PVTSLT(parsed_data)
+ elif parsed_data.identity in (
+ "ADRNAV",
+ "ADRNAVH",
+ "PPPNAV",
+ "SPPNAV",
+ "SPPNAVH",
+ ):
+ self._process_ADRNAV(parsed_data)
+ elif parsed_data.identity in ("SATSINFO",):
+ self._process_SATSINFO(parsed_data)
+ elif parsed_data.identity in ("SATELLITE",):
+ self._process_SATELLITE(parsed_data)
+ elif parsed_data.identity in ("STADOP", "ADRDOP", "PPPDOP"):
+ self._process_STADOP(parsed_data)
+
+ def _process_pos(self, lat: float, lon: float, hmsl: float, undulation: float):
+ """
+ Process lat/lon/hmsl/hae.
+
+ :param float lat: lat
+ :param float lon: lon
+ :param float hmsl: hmsl in meters
+ :param float undulation: separation in meters
+ """
+
+ self.__app.gnss_status.lat = lat
+ self.__app.gnss_status.lon = lon
+ self.__app.gnss_status.alt = hmsl
+ self.__app.gnss_status.hae = hmsl + undulation
+
+ def _process_fix(self, postype: int):
+ """
+ Process fix type.
+
+ :param int postype: attribute representing fix type
+ """
+
+ self.__app.gnss_status.fix = fix2desc("BESTNAV", postype)
+ self.__app.gnss_status.diff_corr = ("RTK" in self.__app.gnss_status.fix) or (
+ "PPP" in self.__app.gnss_status.fix
+ )
+
+ def _process_BESTNAV(self, data: UNIMessage):
+ """
+ Process BESTNAV sentence - Navigation position velocity time solution.
+
+ :param UNIMessage data: BESTNAV parsed message
+ """
+
+ self._process_pos(data.lat, data.lon, data.hmsl, data.undulation)
+ self._process_fix(data.postype)
+ self.__app.gnss_status.sip = data.numsolnsvs
+ self.__app.gnss_status.diff_age = data.diffage
+ self.__app.gnss_status.speed = data.horspd
+ self.__app.gnss_status.track = data.trkgnd
+ self.__app.gnss_status.diff_station = data.stationid
+
+ def _process_PVTSLT(self, data: UNIMessage):
+ """
+ Process PVTSLT sentence - Navigation position velocity time solution.
+
+ :param UNIMessage data: PVTSLT parsed message
+ """
+
+ self._process_pos(
+ data.psrposlat, data.psrposlon, data.psrposhmsl, data.undulation
+ )
+ self._process_fix(data.psrpostype)
+ self.__app.gnss_status.sip = data.psrpossolnsvs
+ self.__app.gnss_status.speed = data.psrvelground
+ self.__app.gnss_status.track = data.headingdegree
+ self.__app.gnss_status.pdop = data.pdop
+ self.__app.gnss_status.hdop = data.hdop
+ self.__app.gnss_status.diff_age = data.bestpos_diffage
+
+ def _process_ADRNAV(self, data: UNIMessage):
+ """
+ Process ADRNAV, PPPNAV, SPPNAV sentences.
+
+ :param UNIMessage data: ADRNAV/PPPNAV/SPPNAV parsed message
+ """
+
+ self._process_pos(data.lat, data.lon, data.hmsl, data.undulation)
+ self._process_fix(data.postype)
+
+ def _process_SATSINFO(self, data: UNIMessage):
+ """
+ Process SATSINFO sentences - Space Vehicle Information for all
+ GNSS constellations.
+
+ :param UNIMessage data: SATSINFO parsed message
+ """
+
+ self.__app.gnss_status.gsv_data = {}
+ num_siv = int(data.numsat)
+ now = time()
+
+ for i in range(num_siv):
+ idx = f"_{i+1:02d}"
+ gnssId = SATSINFO_GNSSID[getattr(data, "sysstatus" + idx + "_01")]
+ svid = getattr(data, "prn" + idx)
+ elev = getattr(data, "elev" + idx)
+ azim = getattr(data, "azi" + idx)
+ cno = getattr(data, "cno" + idx + "_01")
+ self.__app.gnss_status.gsv_data[(gnssId, svid)] = (
+ gnssId,
+ svid,
+ elev,
+ azim,
+ cno,
+ now,
+ )
+
+ self.__app.gnss_status.siv = len(self.__app.gnss_status.gsv_data)
+
+ def _process_SATELLITE(self, data: UNIMessage):
+ """
+ Process SATELLITE sentences - Space Vehicle Information for a
+ specific GNSS constellation (7 in total).
+
+ :param UNIMessage data: SATSINFO parsed message
+ """
+
+ gnssId = SATELLITE_GNSSID[getattr(data, "gnss")]
+ for gnss, prn in list(self.__app.gnss_status.gsv_data.keys()):
+ if gnss == gnssId:
+ self.__app.gnss_status.gsv_data.pop((gnss, prn))
+
+ num_siv = int(data.numsat)
+ now = time()
+ for i in range(num_siv):
+ idx = f"_{i+1:02d}"
+ svid = getattr(data, "prn" + idx)
+ elev = getattr(data, "elv" + idx)
+ azim = getattr(data, "azi" + idx)
+ cno = 0 # cno not available from this message
+ self.__app.gnss_status.gsv_data[(gnssId, svid)] = (
+ gnssId,
+ svid,
+ elev,
+ azim,
+ cno,
+ now,
+ )
+
+ self.__app.gnss_status.siv = len(self.__app.gnss_status.gsv_data)
+
+ def _process_STADOP(self, data: UNIMessage):
+ """
+ Process STADOP/ADRDOP/PPPDOP sentences - DOP Information.
+
+ :param UNIMessage data: STADOP/ADRDOP/PPPDOP parsed message
+ """
+
+ self.__app.gnss_status.pdop = data.pdop
+ self.__app.gnss_status.hdop = data.hdop
+ self.__app.gnss_status.vdop = data.vdop
diff --git a/tests/test_static.py b/tests/test_static.py
index ac8089ee..afb01c3f 100644
--- a/tests/test_static.py
+++ b/tests/test_static.py
@@ -406,17 +406,17 @@ def testwnotow2date(self):
(2263, 518400),
]
dats = [
- "2023-01-01 00:00:00",
- "2005-11-05 00:00:00",
- "2020-08-20 00:00:00",
- "2014-03-16 00:00:00",
- "2023-05-21 00:00:00",
- "2023-05-27 00:00:00",
+ "2022-12-31 23:59:42",
+ "2005-11-04 23:59:43",
+ "2020-08-19 23:59:44",
+ "2014-03-15 23:59:45",
+ "2023-05-20 23:59:46",
+ "2023-05-26 23:59:47",
]
for i, (wno, tow) in enumerate(vals):
- self.assertEqual(str(wnotow2date(wno, tow)), dats[i])
- wno, tow = date2wnotow(datetime(2020, 4, 12))
- self.assertEqual(wnotow2date(wno, tow), datetime(2020, 4, 12))
+ dat = wnotow2date(wno, tow, 18-i)
+ # print(f'"{dat}",')
+ self.assertEqual(str(dat), dats[i])
def testbitsval(self):
bits = [(7, 1), (8, 8), (22, 2), (24, 4), (40, 16)]
@@ -428,13 +428,11 @@ def testbitsval(self):
self.assertEqual(res, EXPECTED_RESULT[i])
def testparserxm(self):
- EXPECTED_RESULT = [
- ("0c00", datetime(1988, 3, 1, 7, 40)),
- ("290900", datetime(1988, 7, 4, 2, 40)),
- ]
+ EXPECTED_RESULT = [('0c00', datetime(1988, 3, 1, 7, 39, 42)), ('290900', datetime(1988, 7, 4, 2, 39, 42))]
RXM_SPARTNKEY = b"\xb5b\x026\x19\x00\x01\x02\x00\x00\x00\x02+\x00\xd0Y\xc8\r\x00\x03+\x00\x00\xdfl\x0e\x0c\x00)\t\x00D;"
msg = UBXReader.parse(RXM_SPARTNKEY)
res = parse_rxmspartnkey(msg)
+ # print(f'"{res}",')
self.assertEqual(res, EXPECTED_RESULT)
def testmapqcompress(self):
From f4b3bc8d3254fd59741d5ea511eb3a6e2202c801 Mon Sep 17 00:00:00 2001
From: SEMU Admin <28569967+semuadmin@users.noreply.github.com>
Date: Fri, 13 Feb 2026 07:34:22 +0000
Subject: [PATCH 4/4] update uni handler
---
INSTALLATION.md | 6 ++++--
README.md | 2 +-
pyproject.toml | 7 ++++++-
src/pygpsclient/chart_frame.py | 3 +++
src/pygpsclient/console_frame.py | 4 +++-
src/pygpsclient/globals.py | 4 ++--
src/pygpsclient/init_presets.py | 6 ++++--
src/pygpsclient/stream_handler.py | 13 ++++++++++---
src/pygpsclient/strings.py | 1 +
src/pygpsclient/uni_handler.py | 31 ++++++++++++++++++++-----------
10 files changed, 54 insertions(+), 23 deletions(-)
diff --git a/INSTALLATION.md b/INSTALLATION.md
index 4c200f0e..836331e5 100644
--- a/INSTALLATION.md
+++ b/INSTALLATION.md
@@ -243,11 +243,13 @@ TBC. Anyone conversant with PowerShell is welcome to contribute an equivalent in
## Troubleshooting
-1. The optional `rasterio` package is only available as an [sdist](#sdist-vs-wheel) on some Linux / ARM platforms (including Raspberry Pi OS), and the consequent [GDAL](https://gdal.org/en/stable/) build and configuration requirements may be problematic e.g. `WARNING:root:Failed to get options via gdal-config`. Refer to [rasterio installation](https://rasterio.readthedocs.io/en/stable/installation.html) and [GDAL installation](https://gdal.org/en/stable/) for assistance but - *be warned* - the process is **not** for the faint-hearted.
+1. `[Errno 13] could not open port /dev/tty**** [Errno 13] permission denied /dev/tty****` error on Linux when attempting to access serial port. Refer to [User Privileges](#userpriv).
+
+1. The optional `rasterio` package is only available as an [sdist](#sdist-vs-wheel) on some Linux / ARM platforms and the consequent [GDAL](https://gdal.org/en/stable/) build and configuration requirements may be problematic e.g. `WARNING:root:Failed to get options via gdal-config`. Refer to [rasterio installation](https://rasterio.readthedocs.io/en/stable/installation.html) and [GDAL installation](https://gdal.org/en/stable/) for assistance but - *be warned* - the process is **not** for the faint-hearted.
In practice, `rasterio` is only required for automatic extents detection in PyGPSClient's Import Custom Map facility. As a workaround, extents can be entered manually, or you can try importing maps on a different platform and then copy-and-paste the relevant `usermaps_l` extents configuration to the target platform.
-1. The optional `cryptography` package is only available as an [sdist](#sdist-vs-wheel) on some 32-bit Linux / ARM platforms (including Raspberry Pi OS), and the consequent OpenSSL build requirements may be problematic e.g. `Building wheel for cryptography (PEP 517): finished with status 'error'`. Refer to [cryptography installation](https://github.com/semuconsulting/pyspartn/blob/main/cryptography_installation/README.md) for assistance.
+1. The optional `cryptography` package is only available as an [sdist](#sdist-vs-wheel) on some 32-bit Linux / ARM platforms and the consequent OpenSSL build requirements may be problematic e.g. `Building wheel for cryptography (PEP 517): finished with status 'error'`. Refer to [cryptography installation](https://github.com/semuconsulting/pyspartn/blob/main/cryptography_installation/README.md) for assistance.
---
## License
diff --git a/README.md b/README.md
index 7f8334d6..180ac940 100644
--- a/README.md
+++ b/README.md
@@ -457,7 +457,7 @@ The `pygnssutils` and `pyubxutils` libraries which underpin many of the function
For further details, refer to the `pygnssutils` homepage at [https://github.com/semuconsulting/pygnssutils](https://github.com/semuconsulting/pygnssutils) or `pyubxutils` homepage at [https://github.com/semuconsulting/pyubxutils](https://github.com/semuconsulting/pyubxutils).
---
+---
## Known Issues
1. Most budget USB-UART adapters (e.g. FT232, CH345, CP2102) have a bandwidth limit of around 3MB/s and may not work reliably above 115200 baud, even if the receiver supports higher baud rates. If you're using an adapter and notice significant message corruption, try reducing the baud rate to a maximum 115200.
diff --git a/pyproject.toml b/pyproject.toml
index 7d33d216..52096614 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -49,7 +49,12 @@ classifiers = [
"Topic :: Scientific/Engineering :: GIS",
]
-dependencies = ["requests>=2.28.0", "Pillow>=9.0.0", "pygnssutils>=1.1.22"]
+dependencies = [
+ "requests>=2.28.0",
+ "Pillow>=9.0.0",
+ "pygnssutils>=1.1.22",
+ "pyunigps>=0.1.4",
+]
[project.scripts]
pygpsclient = "pygpsclient.__main__:main"
diff --git a/src/pygpsclient/chart_frame.py b/src/pygpsclient/chart_frame.py
index 8144f18e..c005b983 100644
--- a/src/pygpsclient/chart_frame.py
+++ b/src/pygpsclient/chart_frame.py
@@ -40,6 +40,7 @@
BGCOL,
ERRCOL,
FGCOL,
+ INFOCOL,
PLOTCOLS,
READONLY,
TRACEMODE_WRITE,
@@ -48,6 +49,7 @@
WIDGETU6,
)
from pygpsclient.helpers import time2str
+from pygpsclient.strings import CONTENTCOPIED
OL_WID = 1
LBLCOL = "white"
@@ -623,6 +625,7 @@ def _on_clipboard(self, event): # pylint: disable=unused-argument
self.__master.clipboard_clear()
self.__master.clipboard_append(csv)
self.__master.update()
+ self.__app.status_label = (CONTENTCOPIED.format("chart"), INFOCOL)
def _on_resize(self, event): # pylint: disable=unused-argument
"""
diff --git a/src/pygpsclient/console_frame.py b/src/pygpsclient/console_frame.py
index c2f7ec0e..21dc45d4 100644
--- a/src/pygpsclient/console_frame.py
+++ b/src/pygpsclient/console_frame.py
@@ -43,9 +43,10 @@
FORMAT_BOTH,
FORMAT_HEXSTR,
FORMAT_HEXTAB,
+ INFOCOL,
WIDGETU3,
)
-from pygpsclient.strings import HALTTAGWARN
+from pygpsclient.strings import CONTENTCOPIED, HALTTAGWARN
HALT = "HALT"
CONSOLELINES = 20
@@ -242,6 +243,7 @@ def _on_clipboard(self, event): # pylint: disable=unused-argument
self.__master.clipboard_clear()
self.__master.clipboard_append(self.txt_console.get("1.0", END))
self.__master.update()
+ self.__app.status_label = (CONTENTCOPIED.format("console"), INFOCOL)
def _on_resize(self, event): # pylint: disable=unused-argument
"""
diff --git a/src/pygpsclient/globals.py b/src/pygpsclient/globals.py
index 7042d4cf..6cacaf20 100644
--- a/src/pygpsclient/globals.py
+++ b/src/pygpsclient/globals.py
@@ -250,8 +250,8 @@
TOPIC_RXM = "/pp/ubx/0236/ip"
TRACK = "track"
TRACEMODE_WRITE = "write"
-TTYOK = ("OK", "$R:", "RESPONSE: OK")
-TTYERR = ("ERROR", "$R?", "RESPONSE: PARSING FAILD")
+TTYOK = ("OK", "$R:")
+TTYERR = ("ERROR", "$R?", "FAIL", "CAN'T FOUND DEVICE")
TTYMARKER = "TTY<<"
UBXPRESETS = "ubxpresets"
UBXSIMULATOR = "ubxsimulator"
diff --git a/src/pygpsclient/init_presets.py b/src/pygpsclient/init_presets.py
index 9b77f52b..8a933d52 100644
--- a/src/pygpsclient/init_presets.py
+++ b/src/pygpsclient/init_presets.py
@@ -100,8 +100,10 @@
"Unicore UM98n Restore Factory Defaults CONFIRM; freset",
"Unicore UM98n Save Current Configuration to NVM CONFIRM; saveconfig",
"Unicore UM98n Force COLD reset CONFIRM; reset",
- "Unicore UM98n Enable NMEA on current port; gpgsa 1; gpgga 1; gpgll 1; gpgsv 4; gpvtg 1; gprmc 1",
- "Unicore UM98n Enable UNI on current port; PVTSLNB 1; SATSINFOB 4; BESTNAV 1; STADOP 1",
+ "Unicore UM98n Enable basic NMEA on current port; gpgsa 1; gpgga 1; gpgll 1; gpgsv 4; gpvtg 1; gprmc 1",
+ "Unicore UM98n Enable basic UNI on current port; PVTSLNB 1; SATSINFOB 4; BESTNAVB 1; STADOPB 1",
+ "Unicore UM98n Disable basic NMEA on current port; unlog gpgsa; unlog gpgga; unlog gpgll; unlog gpgsv; unlog gpvtg; unlog gprmc",
+ "Unicore UM98n Disable basic UNI on current port; unlog PVTSLNB; unlog SATSINFOB; unlog BESTNAVB; unlog STADOPB",
"Unicore UM98n Query Version; version",
"Unicore UM98n Query Config; config",
"Unicore UM98n Stop All Output on COM1; unlog COM1",
diff --git a/src/pygpsclient/stream_handler.py b/src/pygpsclient/stream_handler.py
index 0e245272..d88ca4c2 100644
--- a/src/pygpsclient/stream_handler.py
+++ b/src/pygpsclient/stream_handler.py
@@ -62,7 +62,14 @@ class to read and parse incoming data from the receiver. It places
from pyrtcm import RTCMMessageError, RTCMParseError, RTCMStreamError
from pysbf2 import SBFMessageError, SBFParseError, SBFStreamError
from pyubx2 import ERR_LOG, UBXMessageError, UBXParseError, UBXStreamError
-from pyubxutils import UBXSimulator
+
+try:
+ from pyubxutils import UBXSimulator
+
+ HASUBXUTILS = True
+except (ImportError, ModuleNotFoundError):
+ HASUBXUTILS = False
+
from serial import Serial, SerialException, SerialTimeoutException
from pygpsclient.globals import (
@@ -148,7 +155,7 @@ def _read_thread(
conntype = settings["conntype"]
inactivity_timeout = settings.get("inactivity_timeout", 0)
if conntype == CONNECTED:
- if settings["serial_settings"].port == UBXSIMULATOR:
+ if HASUBXUTILS and settings["serial_settings"].port == UBXSIMULATOR:
conntype = CONNECTED_SIMULATOR
ttydelay = (
self.__app.configuration.get("ttydelay_b")
@@ -238,7 +245,7 @@ def _read_thread(
inactivity_timeout,
)
- elif conntype == CONNECTED_SIMULATOR:
+ elif HASUBXUTILS and conntype == CONNECTED_SIMULATOR:
with UBXSimulator() as stream:
if settings["protocol"] & TTY_PROTOCOL:
self._readlooptty(
diff --git a/src/pygpsclient/strings.py b/src/pygpsclient/strings.py
index e6aef7f0..c86d0790 100644
--- a/src/pygpsclient/strings.py
+++ b/src/pygpsclient/strings.py
@@ -41,6 +41,7 @@
CONFIGRXM = "{} polled, {} key(s) loaded"
CONFIGTITLE = "Config File"
CONFIRM = "CONFIRM"
+CONTENTCOPIED = "Contents of {} copied to clipboard"
DGPSYES = "\u2713" # tick symbol
ENDOFFILE = "End of file reached"
FILEOPENERROR = "Error opening file {}"
diff --git a/src/pygpsclient/uni_handler.py b/src/pygpsclient/uni_handler.py
index 8b5ce2c4..fd727380 100644
--- a/src/pygpsclient/uni_handler.py
+++ b/src/pygpsclient/uni_handler.py
@@ -74,13 +74,11 @@ def process_data(self, raw_data: bytes, parsed_data: UNIMessage):
if raw_data is None:
return
# self.logger.debug(f"data received {parsed_data.identity}")
- self.__app.gnss_status.utc = wnotow2date(
- parsed_data.wno, int(parsed_data.tow / 1000), parsed_data.leapsecond
- ) # datetime.time
+ self._process_utc(parsed_data)
if parsed_data.identity in ("BESTNAV", "BESTNAVH"):
self._process_BESTNAV(parsed_data)
- elif parsed_data.identity in ("PVTSLT",):
- self._process_PVTSLT(parsed_data)
+ elif parsed_data.identity in ("PVTSLN",):
+ self._process_PVTSLN(parsed_data)
elif parsed_data.identity in (
"ADRNAV",
"ADRNAVH",
@@ -96,6 +94,17 @@ def process_data(self, raw_data: bytes, parsed_data: UNIMessage):
elif parsed_data.identity in ("STADOP", "ADRDOP", "PPPDOP"):
self._process_STADOP(parsed_data)
+ def _process_utc(self, data: UNIMessage):
+ """
+ Process wno, tow and leapsecond from UNI message header.
+
+ :param UNIMessage data: parsed message
+ """
+
+ self.__app.gnss_status.utc = wnotow2date(
+ data.wno, int(data.tow / 1000), data.leapsecond
+ )
+
def _process_pos(self, lat: float, lon: float, hmsl: float, undulation: float):
"""
Process lat/lon/hmsl/hae.
@@ -119,8 +128,8 @@ def _process_fix(self, postype: int):
"""
self.__app.gnss_status.fix = fix2desc("BESTNAV", postype)
- self.__app.gnss_status.diff_corr = ("RTK" in self.__app.gnss_status.fix) or (
- "PPP" in self.__app.gnss_status.fix
+ self.__app.gnss_status.diff_corr = (
+ sum(c in self.__app.gnss_status.fix for c in ("RTK", "PPP", "SBAS")) > 0
)
def _process_BESTNAV(self, data: UNIMessage):
@@ -138,11 +147,11 @@ def _process_BESTNAV(self, data: UNIMessage):
self.__app.gnss_status.track = data.trkgnd
self.__app.gnss_status.diff_station = data.stationid
- def _process_PVTSLT(self, data: UNIMessage):
+ def _process_PVTSLN(self, data: UNIMessage):
"""
- Process PVTSLT sentence - Navigation position velocity time solution.
+ Process PVTSLN sentence - Navigation position velocity time solution.
- :param UNIMessage data: PVTSLT parsed message
+ :param UNIMessage data: PVTSLN parsed message
"""
self._process_pos(
@@ -154,7 +163,7 @@ def _process_PVTSLT(self, data: UNIMessage):
self.__app.gnss_status.track = data.headingdegree
self.__app.gnss_status.pdop = data.pdop
self.__app.gnss_status.hdop = data.hdop
- self.__app.gnss_status.diff_age = data.bestpos_diffage
+ self.__app.gnss_status.diff_age = data.bestposdiffage
def _process_ADRNAV(self, data: UNIMessage):
"""